base creada

This commit is contained in:
Abraham
2026-01-10 21:12:17 -08:00
parent 8bfc7d60c3
commit 9adadbd354
62 changed files with 5392 additions and 22447 deletions

View File

@@ -1,642 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:nethive_neo/providers/nethive/empresas_negocios_provider.dart';
import 'package:nethive_neo/pages/empresa_negocios/widgets/empresa_selector_sidebar.dart';
import 'package:nethive_neo/pages/empresa_negocios/widgets/negocios_table.dart';
import 'package:nethive_neo/pages/empresa_negocios/widgets/negocios_cards_view.dart';
import 'package:nethive_neo/pages/empresa_negocios/widgets/mobile_empresa_selector.dart';
import 'package:nethive_neo/pages/empresa_negocios/widgets/negocios_map_view.dart';
import 'package:nethive_neo/theme/theme.dart';
class EmpresaNegociosPage extends StatefulWidget {
const EmpresaNegociosPage({Key? key}) : super(key: key);
@override
State<EmpresaNegociosPage> createState() => _EmpresaNegociosPageState();
}
class _EmpresaNegociosPageState extends State<EmpresaNegociosPage>
with TickerProviderStateMixin {
bool showMapView = false;
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
_slideAnimation = Tween<Offset>(
begin: const Offset(0, 0.3),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeOutCubic,
));
_animationController.forward();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final isLargeScreen = MediaQuery.of(context).size.width > 1200;
return Scaffold(
backgroundColor: AppTheme.of(context).primaryBackground,
body: Container(
decoration: BoxDecoration(
gradient: AppTheme.of(context).darkBackgroundGradient,
),
child: Consumer<EmpresasNegociosProvider>(
builder: (context, provider, child) {
if (isLargeScreen) {
// Vista de escritorio
return Row(
children: [
// Sidebar izquierdo con empresas
SlideTransition(
position: Tween<Offset>(
begin: const Offset(-1, 0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeOutCubic,
)),
child: SizedBox(
width: 320,
child: EmpresaSelectorSidebar(
provider: provider,
onEmpresaSelected: (empresaId) {
provider.setEmpresaSeleccionada(empresaId);
},
),
),
),
// Área principal
Expanded(
child: SlideTransition(
position: _slideAnimation,
child: FadeTransition(
opacity: _fadeAnimation,
child: Container(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header con título y switch
_buildEnhancedHeader(provider),
const SizedBox(height: 24),
// Contenido principal (tabla o mapa)
Expanded(
child: showMapView
? _buildMapView()
: _buildTableView(provider),
),
],
),
),
),
),
),
],
);
} else {
// Vista móvil/tablet
return Column(
children: [
// Header móvil
_buildMobileHeader(provider),
// Contenido principal
Expanded(
child: showMapView
? _buildMapView()
: NegociosCardsView(provider: provider),
),
],
);
}
},
),
),
// FAB para vista móvil
floatingActionButton: MediaQuery.of(context).size.width <= 800
? _buildMobileFAB(context)
: null,
);
}
Widget _buildEnhancedHeader(EmpresasNegociosProvider provider) {
return Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
gradient: AppTheme.of(context).primaryGradient,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: AppTheme.of(context).primaryColor.withOpacity(0.3),
blurRadius: 20,
offset: const Offset(0, 8),
),
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Row(
children: [
// Icono animado
TweenAnimationBuilder<double>(
duration: const Duration(milliseconds: 1000),
tween: Tween(begin: 0.0, end: 1.0),
builder: (context, value, child) {
return Transform.rotate(
angle: value * 0.1,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: Colors.white.withOpacity(0.3),
width: 2,
),
),
child: Icon(
Icons.business_center,
color: Colors.white,
size: 32,
),
),
);
},
),
const SizedBox(width: 20),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Título principal con gradiente
ShaderMask(
shaderCallback: (bounds) => LinearGradient(
colors: [Colors.white, Colors.white.withOpacity(0.8)],
).createShader(bounds),
child: Text(
provider.empresaSeleccionada != null
? 'Sucursales de ${provider.empresaSeleccionada!.nombre}'
: 'Gestión de Empresas y Sucursales',
style: const TextStyle(
color: Colors.white,
fontSize: 28,
fontWeight: FontWeight.bold,
letterSpacing: 0.5,
),
),
),
const SizedBox(height: 12),
if (provider.empresaSeleccionada != null) ...[
Row(
children: [
// Badge animado
TweenAnimationBuilder<double>(
duration: const Duration(milliseconds: 600),
tween: Tween(begin: 0.0, end: 1.0),
builder: (context, value, child) {
return Transform.scale(
scale: value,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.9),
borderRadius: BorderRadius.circular(25),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.store,
color: AppTheme.of(context).primaryColor,
size: 18,
),
const SizedBox(width: 8),
Text(
'${provider.negocios.length} sucursales',
style: TextStyle(
color: AppTheme.of(context).primaryColor,
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
],
),
),
);
},
),
const SizedBox(width: 16),
Expanded(
child: Text(
provider.empresaSeleccionada!.rfc,
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
],
),
],
],
),
),
// Switch mejorado para cambiar vista
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: Colors.white.withOpacity(0.2),
),
),
child: Column(
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.table_chart,
color: Colors.white,
size: 20,
),
const SizedBox(width: 8),
Text(
'Vista',
style: TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
const SizedBox(width: 8),
Icon(
Icons.map,
color: Colors.white,
size: 20,
),
],
),
const SizedBox(height: 12),
Switch(
value: showMapView,
onChanged: (value) {
setState(() {
showMapView = value;
});
},
activeColor: Colors.white,
activeTrackColor: Colors.white.withOpacity(0.3),
inactiveThumbColor: Colors.white.withOpacity(0.7),
inactiveTrackColor: Colors.white.withOpacity(0.1),
),
],
),
),
],
),
);
}
Widget _buildMobileHeader(EmpresasNegociosProvider provider) {
return Container(
padding: const EdgeInsets.fromLTRB(20, 40, 20, 20),
decoration: BoxDecoration(
gradient: AppTheme.of(context).primaryGradient,
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(30),
bottomRight: Radius.circular(30),
),
boxShadow: [
BoxShadow(
color: AppTheme.of(context).primaryColor.withOpacity(0.3),
blurRadius: 15,
offset: const Offset(0, 5),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
Icons.business,
color: Colors.white,
size: 24,
),
),
const SizedBox(width: 16),
Expanded(
child: Text(
'NETHIVE',
style: const TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
letterSpacing: 1.2,
),
),
),
// Switch para modo vista
Switch(
value: showMapView,
onChanged: (value) {
setState(() {
showMapView = value;
});
},
activeColor: Colors.white,
),
],
),
if (provider.empresaSeleccionada != null) ...[
const SizedBox(height: 16),
Text(
provider.empresaSeleccionada!.nombre,
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
),
child: Text(
'${provider.negocios.length} sucursales',
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
),
],
],
),
);
}
Widget _buildTableView(EmpresasNegociosProvider provider) {
if (provider.empresaSeleccionada == null) {
return _buildEmptyState();
}
return Container(
decoration: BoxDecoration(
color: AppTheme.of(context).secondaryBackground,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: AppTheme.of(context).primaryColor.withOpacity(0.2),
width: 1,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 15,
offset: const Offset(0, 5),
),
],
),
child: Column(
children: [
// Header de la tabla mejorado
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: AppTheme.of(context).modernGradient,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.store,
color: Colors.white,
size: 20,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Sucursales',
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
letterSpacing: 0.5,
),
),
Text(
'Gestión y control de ubicaciones',
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 14,
),
),
],
),
),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
),
child: Text(
'${provider.negocios.length} registros',
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
// Tabla de negocios
Expanded(
child: NegociosTable(provider: provider),
),
],
),
);
}
Widget _buildEmptyState() {
return Center(
child: TweenAnimationBuilder<double>(
duration: const Duration(milliseconds: 1000),
tween: Tween(begin: 0.0, end: 1.0),
builder: (context, value, child) {
return Transform.scale(
scale: value,
child: Container(
padding: const EdgeInsets.all(40),
decoration: BoxDecoration(
gradient: AppTheme.of(context).primaryGradient,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: AppTheme.of(context).primaryColor.withOpacity(0.3),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.business_center,
size: 80,
color: Colors.white,
),
const SizedBox(height: 20),
Text(
'Selecciona una empresa',
style: const TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
Text(
'Elige una empresa del panel lateral\npara ver y gestionar sus sucursales',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 16,
),
),
],
),
),
);
},
),
);
}
Widget _buildMapView() {
return Consumer<EmpresasNegociosProvider>(
builder: (context, provider, child) {
return NegociosMapView(provider: provider);
},
);
}
Widget _buildMobileFAB(BuildContext context) {
return Consumer<EmpresasNegociosProvider>(
builder: (context, provider, child) {
return FloatingActionButton.extended(
onPressed: () {
_showMobileEmpresaSelector(context, provider);
},
backgroundColor: AppTheme.of(context).primaryColor,
icon: const Icon(Icons.business, color: Colors.white),
label: Text(
provider.empresaSeleccionada != null
? provider.empresaSeleccionada!.nombre.length > 15
? '${provider.empresaSeleccionada!.nombre.substring(0, 15)}...'
: provider.empresaSeleccionada!.nombre
: 'Empresas',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
);
},
);
}
void _showMobileEmpresaSelector(
BuildContext context, EmpresasNegociosProvider provider) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => DraggableScrollableSheet(
initialChildSize: 0.7,
maxChildSize: 0.95,
minChildSize: 0.3,
builder: (context, scrollController) => MobileEmpresaSelector(
provider: provider,
onEmpresaSelected: (empresaId) {
provider.setEmpresaSeleccionada(empresaId);
},
),
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,165 +0,0 @@
import 'package:flutter/material.dart';
import 'package:nethive_neo/providers/nethive/empresas_negocios_provider.dart';
import 'package:nethive_neo/theme/theme.dart';
import 'empresa_dialog_animations.dart';
import 'empresa_dialog_header.dart';
import 'empresa_dialog_form.dart';
class AddEmpresaDialog extends StatefulWidget {
final EmpresasNegociosProvider provider;
const AddEmpresaDialog({
Key? key,
required this.provider,
}) : super(key: key);
@override
State<AddEmpresaDialog> createState() => _AddEmpresaDialogState();
}
class _AddEmpresaDialogState extends State<AddEmpresaDialog>
with TickerProviderStateMixin {
late EmpresaDialogAnimations _animations;
@override
void initState() {
super.initState();
_animations = EmpresaDialogAnimations(vsync: this);
_animations.initialize();
// Escuchar cambios del provider
widget.provider.addListener(_onProviderChanged);
}
void _onProviderChanged() {
if (mounted) {
setState(() {
// Forzar rebuild cuando cambie el provider
});
}
}
@override
void dispose() {
widget.provider.removeListener(_onProviderChanged);
_animations.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (!_animations.isInitialized) {
return const SizedBox.shrink();
}
// Detectar el tamaño de pantalla
final screenSize = MediaQuery.of(context).size;
final isDesktop = screenSize.width > 1024;
final isTablet = screenSize.width > 768 && screenSize.width <= 1024;
// Ajustar dimensiones según el tipo de pantalla
final maxWidth = isDesktop ? 900.0 : (isTablet ? 750.0 : 650.0);
final maxHeight = isDesktop ? 700.0 : 750.0;
return AnimatedBuilder(
animation: _animations.combinedAnimation,
builder: (context, child) {
return FadeTransition(
opacity: _animations.fadeAnimation,
child: Dialog(
backgroundColor: Colors.transparent,
insetPadding: EdgeInsets.all(isDesktop ? 40 : 20),
child: Transform.scale(
scale: _animations.scaleAnimation.value,
child: Container(
constraints: BoxConstraints(
maxWidth: maxWidth,
maxHeight: maxHeight,
minHeight: isDesktop ? 600 : 400,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(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(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(),
),
),
),
),
),
);
},
);
}
Widget _buildDesktopLayout() {
return Row(
children: [
// Header lateral compacto para desktop
EmpresaDialogHeader(
isDesktop: true,
slideAnimation: _animations.slideAnimation,
),
// Contenido principal del formulario
Expanded(
child: EmpresaDialogForm(
provider: widget.provider,
isDesktop: true,
),
),
],
);
}
Widget _buildMobileLayout() {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
// Header para móvil
EmpresaDialogHeader(
isDesktop: false,
slideAnimation: _animations.slideAnimation,
),
// Contenido del formulario para móvil
Flexible(
child: EmpresaDialogForm(
provider: widget.provider,
isDesktop: false,
),
),
],
);
}
}

View File

@@ -1,126 +0,0 @@
import 'package:flutter/material.dart';
import 'package:nethive_neo/theme/theme.dart';
class EmpresaActionButtons extends StatelessWidget {
final bool isLoading;
final bool isDesktop;
final VoidCallback onCancel;
final VoidCallback onSubmit;
const EmpresaActionButtons({
Key? key,
required this.isLoading,
required this.isDesktop,
required this.onCancel,
required this.onSubmit,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Row(
children: [
// Botón Cancelar
Expanded(
child: Container(
height: isDesktop ? 45 : 50,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: AppTheme.of(context).secondaryText.withOpacity(0.3),
width: 2,
),
),
child: TextButton(
onPressed: isLoading ? null : onCancel,
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: 16),
// Botón Crear Empresa
Expanded(
flex: 2,
child: Container(
height: isDesktop ? 45 : 50,
decoration: BoxDecoration(
gradient: AppTheme.of(context).primaryGradient,
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: AppTheme.of(context).primaryColor.withOpacity(0.4),
blurRadius: 15,
offset: const Offset(0, 5),
spreadRadius: 2,
),
],
),
child: ElevatedButton(
onPressed: isLoading ? null : onSubmit,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
),
child: isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.business_center_rounded,
color: Colors.white,
size: 20,
),
const SizedBox(width: 10),
Text(
'Crear Empresa',
style: TextStyle(
color: Colors.white,
fontSize: isDesktop ? 14 : 16,
fontWeight: FontWeight.bold,
letterSpacing: 0.5,
),
),
],
),
),
),
),
],
);
}
}

View File

@@ -1,79 +0,0 @@
import 'package:flutter/material.dart';
class EmpresaDialogAnimations {
final TickerProvider vsync;
late AnimationController _scaleController;
late AnimationController _slideController;
late AnimationController _fadeController;
late Animation<double> _scaleAnimation;
late Animation<Offset> _slideAnimation;
late Animation<double> _fadeAnimation;
late Listenable _combinedAnimation;
bool _isInitialized = false;
EmpresaDialogAnimations({required this.vsync});
// Getters para acceder a las animaciones
Animation<double> get scaleAnimation => _scaleAnimation;
Animation<Offset> get slideAnimation => _slideAnimation;
Animation<double> get fadeAnimation => _fadeAnimation;
Listenable get combinedAnimation => _combinedAnimation;
bool get isInitialized => _isInitialized;
void initialize() {
_scaleController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: vsync,
);
_slideController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: vsync,
);
_fadeController = AnimationController(
duration: const Duration(milliseconds: 400),
vsync: vsync,
);
_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,
);
_combinedAnimation =
Listenable.merge([_scaleAnimation, _slideAnimation, _fadeAnimation]);
// Pequeño delay para asegurar que el widget esté completamente montado
WidgetsBinding.instance.addPostFrameCallback((_) {
_isInitialized = true;
_startAnimations();
});
}
void _startAnimations() {
_fadeController.forward();
Future.delayed(const Duration(milliseconds: 100), () {
_scaleController.forward();
});
Future.delayed(const Duration(milliseconds: 200), () {
_slideController.forward();
});
}
void dispose() {
_scaleController.dispose();
_slideController.dispose();
_fadeController.dispose();
}
}

View File

@@ -1,228 +0,0 @@
import 'package:flutter/material.dart';
import 'package:nethive_neo/providers/nethive/empresas_negocios_provider.dart';
import 'package:nethive_neo/theme/theme.dart';
import 'empresa_form_fields.dart';
import 'empresa_file_section.dart';
import 'empresa_action_buttons.dart';
class EmpresaDialogForm extends StatefulWidget {
final EmpresasNegociosProvider provider;
final bool isDesktop;
const EmpresaDialogForm({
Key? key,
required this.provider,
required this.isDesktop,
}) : super(key: key);
@override
State<EmpresaDialogForm> createState() => _EmpresaDialogFormState();
}
class _EmpresaDialogFormState extends State<EmpresaDialogForm> {
final _formKey = GlobalKey<FormState>();
final _nombreController = TextEditingController();
final _rfcController = TextEditingController();
final _direccionController = TextEditingController();
final _telefonoController = TextEditingController();
final _emailController = TextEditingController();
bool _isLoading = false;
@override
void dispose() {
_nombreController.dispose();
_rfcController.dispose();
_direccionController.dispose();
_telefonoController.dispose();
_emailController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(25),
child: Form(
key: _formKey,
child: widget.isDesktop ? _buildDesktopForm() : _buildMobileForm(),
),
);
}
Widget _buildDesktopForm() {
return Column(
children: [
Expanded(
child: SingleChildScrollView(
child: Column(
children: [
// Campos del formulario en filas para desktop
EmpresaFormFields(
isDesktop: true,
nombreController: _nombreController,
rfcController: _rfcController,
direccionController: _direccionController,
telefonoController: _telefonoController,
emailController: _emailController,
),
const SizedBox(height: 20),
// Sección de archivos
EmpresaFileSection(
provider: widget.provider,
isDesktop: true,
),
const SizedBox(height: 25),
// Botones de acción
EmpresaActionButtons(
isLoading: _isLoading,
isDesktop: true,
onCancel: () => _handleCancel(),
onSubmit: () => _crearEmpresa(),
),
],
),
),
),
],
);
}
Widget _buildMobileForm() {
return SingleChildScrollView(
child: Column(
children: [
// Campos del formulario en columnas para móvil
EmpresaFormFields(
isDesktop: false,
nombreController: _nombreController,
rfcController: _rfcController,
direccionController: _direccionController,
telefonoController: _telefonoController,
emailController: _emailController,
),
const SizedBox(height: 20),
// Sección de archivos
EmpresaFileSection(
provider: widget.provider,
isDesktop: false,
),
const SizedBox(height: 25),
// Botones de acción
EmpresaActionButtons(
isLoading: _isLoading,
isDesktop: false,
onCancel: () => _handleCancel(),
onSubmit: () => _crearEmpresa(),
),
],
),
);
}
void _handleCancel() {
widget.provider.resetFormData();
Navigator.of(context).pop();
}
Future<void> _crearEmpresa() async {
if (!_formKey.currentState!.validate()) return;
setState(() {
_isLoading = true;
});
try {
final success = await widget.provider.crearEmpresa(
nombre: _nombreController.text.trim(),
rfc: _rfcController.text.trim(),
direccion: _direccionController.text.trim(),
telefono: _telefonoController.text.trim(),
email: _emailController.text.trim(),
);
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(
'Empresa creada 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 crear la empresa',
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;
});
}
}
}
}

View File

@@ -1,171 +0,0 @@
import 'package:flutter/material.dart';
import 'package:nethive_neo/theme/theme.dart';
class EmpresaDialogHeader extends StatelessWidget {
final bool isDesktop;
final Animation<Offset> slideAnimation;
const EmpresaDialogHeader({
Key? key,
required this.isDesktop,
required this.slideAnimation,
}) : super(key: key);
@override
Widget build(BuildContext context) {
if (isDesktop) {
return _buildDesktopHeader(context);
} else {
return _buildMobileHeader(context);
}
}
Widget _buildDesktopHeader(BuildContext context) {
return Container(
width: 280,
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: [
_buildIcon(context),
const SizedBox(height: 20),
_buildTitle(context, fontSize: 24),
const SizedBox(height: 8),
_buildSubtitle(context, fontSize: 14),
],
),
),
),
);
}
Widget _buildMobileHeader(BuildContext context) {
return 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: [
_buildIcon(context),
const SizedBox(height: 16),
_buildTitle(context, fontSize: 26),
const SizedBox(height: 8),
_buildSubtitle(context, fontSize: 14, isMobile: true),
],
),
),
);
}
Widget _buildIcon(BuildContext context) {
return Container(
padding: EdgeInsets.all(isDesktop ? 20 : 18),
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: Icon(
Icons.business_center_rounded,
color: Colors.white,
size: isDesktop ? 35 : 35,
),
);
}
Widget _buildTitle(BuildContext context, {required double fontSize}) {
return Text(
'Nueva Empresa',
style: TextStyle(
color: Colors.white,
fontSize: fontSize,
fontWeight: FontWeight.bold,
letterSpacing: 1.5,
),
textAlign: TextAlign.center,
);
}
Widget _buildSubtitle(BuildContext context,
{required double fontSize, bool isMobile = false}) {
return 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(
isMobile
? '✨ Registra una nueva empresa en tu sistema'
: '✨ Registra una nueva empresa',
style: TextStyle(
color: Colors.white.withOpacity(0.95),
fontSize: fontSize,
fontWeight: FontWeight.w500,
letterSpacing: 0.5,
),
textAlign: TextAlign.center,
),
);
}
}

View File

@@ -1,425 +0,0 @@
import 'package:flutter/material.dart';
import 'package:nethive_neo/providers/nethive/empresas_negocios_provider.dart';
import 'package:nethive_neo/theme/theme.dart';
class EmpresaFileSection extends StatelessWidget {
final EmpresasNegociosProvider provider;
final bool isDesktop;
const EmpresaFileSection({
Key? key,
required this.provider,
required this.isDesktop,
}) : super(key: key);
@override
Widget build(BuildContext context) {
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,
),
boxShadow: [
BoxShadow(
color: AppTheme.of(context).primaryColor.withOpacity(0.2),
blurRadius: 15,
offset: const Offset(0, 8),
spreadRadius: 2,
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header de la sección
_buildSectionHeader(context),
const SizedBox(height: 16),
// Botones de archivos
if (isDesktop)
_buildDesktopFileButtons(context)
else
_buildMobileFileButtons(context),
],
),
);
}
Widget _buildSectionHeader(BuildContext context) {
return Row(
children: [
Container(
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.4),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: const Icon(
Icons.cloud_upload_rounded,
color: Colors.white,
size: 18,
),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Archivos Opcionales',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: AppTheme.of(context).primaryText,
),
),
Text(
'Logo e imagen de la empresa',
style: TextStyle(
fontSize: 12,
color: AppTheme.of(context).secondaryText,
),
),
],
),
],
);
}
Widget _buildDesktopFileButtons(BuildContext context) {
return Row(
children: [
Expanded(
child: _buildCompactFileButton(
context: context,
label: 'Logo de la empresa',
subtitle: 'PNG, JPG (Max 2MB)',
icon: Icons.image_rounded,
fileName: provider.logoFileName,
file: provider.logoToUpload,
onPressed: provider.selectLogo,
gradient: LinearGradient(
colors: [Colors.blue.shade400, Colors.blue.shade600],
),
),
),
const SizedBox(width: 16),
Expanded(
child: _buildCompactFileButton(
context: context,
label: 'Imagen principal',
subtitle: 'Imagen representativa',
icon: Icons.photo_library_rounded,
fileName: provider.imagenFileName,
file: provider.imagenToUpload,
onPressed: provider.selectImagen,
gradient: LinearGradient(
colors: [Colors.purple.shade400, Colors.purple.shade600],
),
),
),
],
);
}
Widget _buildMobileFileButtons(BuildContext context) {
return Column(
children: [
_buildEnhancedFileButton(
context: context,
label: 'Logo de la empresa',
subtitle: 'Formato PNG, JPG (Max 2MB)',
icon: Icons.image_rounded,
fileName: provider.logoFileName,
file: provider.logoToUpload,
onPressed: provider.selectLogo,
gradient: LinearGradient(
colors: [Colors.blue.shade400, Colors.blue.shade600],
),
),
const SizedBox(height: 12),
_buildEnhancedFileButton(
context: context,
label: 'Imagen principal',
subtitle: 'Imagen representativa de la empresa',
icon: Icons.photo_library_rounded,
fileName: provider.imagenFileName,
file: provider.imagenToUpload,
onPressed: provider.selectImagen,
gradient: LinearGradient(
colors: [Colors.purple.shade400, Colors.purple.shade600],
),
),
],
);
}
Widget _buildCompactFileButton({
required BuildContext context,
required String label,
required String subtitle,
required IconData icon,
required String? fileName,
required dynamic file,
required VoidCallback onPressed,
required Gradient gradient,
}) {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppTheme.of(context).primaryColor.withOpacity(0.3),
width: 2,
),
boxShadow: [
BoxShadow(
color: AppTheme.of(context).primaryColor.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onPressed,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
children: [
// Icono con gradiente
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
gradient: gradient,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 8,
offset: const Offset(0, 3),
),
],
),
child: Icon(
icon,
color: Colors.white,
size: 20,
),
),
const SizedBox(height: 8),
// Información del archivo
Text(
label,
style: TextStyle(
color: AppTheme.of(context).primaryText,
fontWeight: FontWeight.bold,
fontSize: 13,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 4),
Text(
fileName ?? subtitle,
style: TextStyle(
color: fileName != null
? AppTheme.of(context).primaryColor
: AppTheme.of(context).secondaryText,
fontSize: 11,
fontWeight:
fileName != null ? FontWeight.w600 : FontWeight.normal,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),
// Preview de imagen si existe
if (file != null) ...[
const SizedBox(height: 8),
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(
color:
AppTheme.of(context).primaryColor.withOpacity(0.4),
width: 2,
),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(6),
child: provider.getImageWidget(
file,
height: 40,
width: 40,
),
),
),
],
],
),
),
),
),
);
}
Widget _buildEnhancedFileButton({
required BuildContext context,
required String label,
required String subtitle,
required IconData icon,
required String? fileName,
required dynamic file,
required VoidCallback onPressed,
required Gradient gradient,
}) {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: AppTheme.of(context).primaryColor.withOpacity(0.3),
width: 2,
),
boxShadow: [
BoxShadow(
color: AppTheme.of(context).primaryColor.withOpacity(0.1),
blurRadius: 15,
offset: const Offset(0, 6),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onPressed,
borderRadius: BorderRadius.circular(16),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
// Icono con gradiente
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
gradient: gradient,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Icon(
icon,
color: Colors.white,
size: 24,
),
),
const SizedBox(width: 16),
// Información del archivo
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
color: AppTheme.of(context).primaryText,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 4),
Text(
fileName ?? subtitle,
style: TextStyle(
color: fileName != null
? AppTheme.of(context).primaryColor
: AppTheme.of(context).secondaryText,
fontSize: 13,
fontWeight: fileName != null
? FontWeight.w600
: FontWeight.normal,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
// Preview de imagen si existe
if (file != null) ...[
const SizedBox(width: 16),
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
border: Border.all(
color:
AppTheme.of(context).primaryColor.withOpacity(0.4),
width: 2,
),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: provider.getImageWidget(
file,
height: 50,
width: 50,
),
),
),
] else ...[
const SizedBox(width: 16),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppTheme.of(context).primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.upload_file,
color: AppTheme.of(context).primaryColor,
size: 20,
),
),
],
],
),
),
),
),
);
}
}

View File

@@ -1,301 +0,0 @@
import 'package:flutter/material.dart';
import 'package:nethive_neo/theme/theme.dart';
class EmpresaFormFields extends StatelessWidget {
final bool isDesktop;
final TextEditingController nombreController;
final TextEditingController rfcController;
final TextEditingController direccionController;
final TextEditingController telefonoController;
final TextEditingController emailController;
const EmpresaFormFields({
Key? key,
required this.isDesktop,
required this.nombreController,
required this.rfcController,
required this.direccionController,
required this.telefonoController,
required this.emailController,
}) : super(key: key);
@override
Widget build(BuildContext context) {
if (isDesktop) {
return _buildDesktopFields(context);
} else {
return _buildMobileFields(context);
}
}
Widget _buildDesktopFields(BuildContext context) {
return Column(
children: [
// Primera fila - Nombre y RFC
Row(
children: [
Expanded(
flex: 2,
child: _buildFormField(
context: context,
controller: nombreController,
label: 'Nombre de la empresa',
hint: 'Ej: TechCorp Solutions S.A.',
icon: Icons.business_rounded,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'El nombre es requerido';
}
return null;
},
),
),
const SizedBox(width: 16),
Expanded(
child: _buildFormField(
context: context,
controller: rfcController,
label: 'RFC',
hint: 'Ej: ABC123456789',
icon: Icons.assignment_rounded,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'El RFC es requerido';
}
return null;
},
),
),
],
),
const SizedBox(height: 16),
// Segunda fila - Dirección
_buildFormField(
context: context,
controller: direccionController,
label: 'Dirección',
hint: 'Dirección completa de la empresa',
icon: Icons.location_on_rounded,
maxLines: 2,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'La dirección es requerida';
}
return null;
},
),
const SizedBox(height: 16),
// Tercera fila - Teléfono y Email
Row(
children: [
Expanded(
child: _buildFormField(
context: context,
controller: telefonoController,
label: 'Teléfono',
hint: 'Ej: +52 555 123 4567',
icon: Icons.phone_rounded,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'El teléfono es requerido';
}
return null;
},
),
),
const SizedBox(width: 16),
Expanded(
child: _buildFormField(
context: context,
controller: emailController,
label: 'Email',
hint: 'contacto@empresa.com',
icon: Icons.email_rounded,
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'El email es requerido';
}
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$')
.hasMatch(value)) {
return 'Email inválido';
}
return null;
},
),
),
],
),
],
);
}
Widget _buildMobileFields(BuildContext context) {
return Column(
children: [
_buildFormField(
context: context,
controller: nombreController,
label: 'Nombre de la empresa',
hint: 'Ej: TechCorp Solutions S.A. de C.V.',
icon: Icons.business_rounded,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'El nombre es requerido';
}
return null;
},
),
_buildFormField(
context: context,
controller: rfcController,
label: 'RFC',
hint: 'Ej: ABC123456789',
icon: Icons.assignment_rounded,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'El RFC es requerido';
}
return null;
},
),
_buildFormField(
context: context,
controller: direccionController,
label: 'Dirección',
hint: 'Dirección completa de la empresa',
icon: Icons.location_on_rounded,
maxLines: 3,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'La dirección es requerida';
}
return null;
},
),
_buildFormField(
context: context,
controller: telefonoController,
label: 'Teléfono',
hint: 'Ej: +52 555 123 4567',
icon: Icons.phone_rounded,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'El teléfono es requerido';
}
return null;
},
),
_buildFormField(
context: context,
controller: emailController,
label: 'Email',
hint: 'contacto@empresa.com',
icon: Icons.email_rounded,
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'El email es requerido';
}
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
return 'Email inválido';
}
return null;
},
),
],
);
}
Widget _buildFormField({
required BuildContext context,
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),
),
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,169 +0,0 @@
import 'package:flutter/material.dart';
import 'package:nethive_neo/providers/nethive/empresas_negocios_provider.dart';
import 'package:nethive_neo/theme/theme.dart';
import 'negocio_dialog_animations.dart';
import 'negocio_dialog_header.dart';
import 'negocio_dialog_form.dart';
class AddNegocioDialog extends StatefulWidget {
final EmpresasNegociosProvider provider;
final String empresaId;
const AddNegocioDialog({
Key? key,
required this.provider,
required this.empresaId,
}) : super(key: key);
@override
State<AddNegocioDialog> createState() => _AddNegocioDialogState();
}
class _AddNegocioDialogState extends State<AddNegocioDialog>
with TickerProviderStateMixin {
late NegocioDialogAnimations _animations;
@override
void initState() {
super.initState();
_animations = NegocioDialogAnimations(vsync: this);
_animations.initialize();
// Escuchar cambios del provider
widget.provider.addListener(_onProviderChanged);
}
void _onProviderChanged() {
if (mounted) {
setState(() {
// Forzar rebuild cuando cambie el provider
});
}
}
@override
void dispose() {
widget.provider.removeListener(_onProviderChanged);
_animations.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (!_animations.isInitialized) {
return const SizedBox.shrink();
}
// Detectar el tamaño de pantalla
final screenSize = MediaQuery.of(context).size;
final isDesktop = screenSize.width > 1024;
final isTablet = screenSize.width > 768 && screenSize.width <= 1024;
// Ajustar dimensiones según el tipo de pantalla
final maxWidth = isDesktop ? 950.0 : (isTablet ? 800.0 : 700.0);
final maxHeight = isDesktop ? 750.0 : 800.0;
return AnimatedBuilder(
animation: _animations.combinedAnimation,
builder: (context, child) {
return FadeTransition(
opacity: _animations.fadeAnimation,
child: Dialog(
backgroundColor: Colors.transparent,
insetPadding: EdgeInsets.all(isDesktop ? 40 : 20),
child: Transform.scale(
scale: _animations.scaleAnimation.value,
child: Container(
constraints: BoxConstraints(
maxWidth: maxWidth,
maxHeight: maxHeight,
minHeight: isDesktop ? 650 : 450,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(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(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(),
),
),
),
),
),
);
},
);
}
Widget _buildDesktopLayout() {
return Row(
children: [
// Header lateral compacto para desktop
NegocioDialogHeader(
isDesktop: true,
slideAnimation: _animations.slideAnimation,
),
// Contenido principal del formulario
Expanded(
child: NegocioDialogForm(
provider: widget.provider,
isDesktop: true,
empresaId: widget.empresaId, // Pasar empresaId al formulario
),
),
],
);
}
Widget _buildMobileLayout() {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
// Header para móvil
NegocioDialogHeader(
isDesktop: false,
slideAnimation: _animations.slideAnimation,
),
// Contenido del formulario para móvil
Flexible(
child: NegocioDialogForm(
provider: widget.provider,
isDesktop: false,
empresaId: widget.empresaId, // Pasar empresaId al formulario
),
),
],
);
}
}

View File

@@ -1,134 +0,0 @@
import 'package:flutter/material.dart';
import 'package:nethive_neo/theme/theme.dart';
class NegocioActionButtons extends StatelessWidget {
final bool isLoading;
final bool isDesktop;
final VoidCallback onCancel;
final VoidCallback onSubmit;
const NegocioActionButtons({
Key? key,
required this.isLoading,
required this.isDesktop,
required this.onCancel,
required this.onSubmit,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Row(
children: [
// Botón Cancelar
Expanded(
child: Container(
height: isDesktop ? 45 : 50,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: AppTheme.of(context).secondaryText.withOpacity(0.3),
width: 2,
),
),
child: TextButton(
onPressed: isLoading ? null : onCancel,
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: 16),
// Botón Crear Negocio
Expanded(
flex: 2,
child: Container(
height: isDesktop ? 45 : 50,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
AppTheme.of(context).tertiaryColor,
AppTheme.of(context).primaryColor,
AppTheme.of(context).secondaryColor,
],
),
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: AppTheme.of(context).primaryColor.withOpacity(0.4),
blurRadius: 15,
offset: const Offset(0, 5),
spreadRadius: 2,
),
],
),
child: ElevatedButton(
onPressed: isLoading ? null : onSubmit,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
),
child: isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.add_business_rounded,
color: Colors.white,
size: 20,
),
const SizedBox(width: 10),
Text(
'Crear Negocio',
style: TextStyle(
color: Colors.white,
fontSize: isDesktop ? 14 : 16,
fontWeight: FontWeight.bold,
letterSpacing: 0.5,
),
),
],
),
),
),
),
],
);
}
}

View File

@@ -1,79 +0,0 @@
import 'package:flutter/material.dart';
class NegocioDialogAnimations {
final TickerProvider vsync;
late AnimationController _scaleController;
late AnimationController _slideController;
late AnimationController _fadeController;
late Animation<double> _scaleAnimation;
late Animation<Offset> _slideAnimation;
late Animation<double> _fadeAnimation;
late Listenable _combinedAnimation;
bool _isInitialized = false;
NegocioDialogAnimations({required this.vsync});
// Getters para acceder a las animaciones
Animation<double> get scaleAnimation => _scaleAnimation;
Animation<Offset> get slideAnimation => _slideAnimation;
Animation<double> get fadeAnimation => _fadeAnimation;
Listenable get combinedAnimation => _combinedAnimation;
bool get isInitialized => _isInitialized;
void initialize() {
_scaleController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: vsync,
);
_slideController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: vsync,
);
_fadeController = AnimationController(
duration: const Duration(milliseconds: 400),
vsync: vsync,
);
_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,
);
_combinedAnimation =
Listenable.merge([_scaleAnimation, _slideAnimation, _fadeAnimation]);
// Pequeño delay para asegurar que el widget esté completamente montado
WidgetsBinding.instance.addPostFrameCallback((_) {
_isInitialized = true;
_startAnimations();
});
}
void _startAnimations() {
_fadeController.forward();
Future.delayed(const Duration(milliseconds: 100), () {
_scaleController.forward();
});
Future.delayed(const Duration(milliseconds: 200), () {
_slideController.forward();
});
}
void dispose() {
_scaleController.dispose();
_slideController.dispose();
_fadeController.dispose();
}
}

View File

@@ -1,232 +0,0 @@
import 'package:flutter/material.dart';
import 'package:nethive_neo/providers/nethive/empresas_negocios_provider.dart';
import 'negocio_form_fields.dart';
import 'negocio_empresa_selector.dart';
import 'negocio_action_buttons.dart';
class NegocioDialogForm extends StatefulWidget {
final EmpresasNegociosProvider provider;
final bool isDesktop;
final String empresaId;
const NegocioDialogForm({
Key? key,
required this.provider,
required this.isDesktop,
required this.empresaId,
}) : super(key: key);
@override
State<NegocioDialogForm> createState() => _NegocioDialogFormState();
}
class _NegocioDialogFormState extends State<NegocioDialogForm> {
final _formKey = GlobalKey<FormState>();
final _nombreController = TextEditingController();
final _direccionController = TextEditingController();
final _latitudController = TextEditingController();
final _longitudController = TextEditingController();
final _tipoLocalController = TextEditingController();
bool _isLoading = false;
@override
void dispose() {
_nombreController.dispose();
_direccionController.dispose();
_latitudController.dispose();
_longitudController.dispose();
_tipoLocalController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(25),
child: Form(
key: _formKey,
child: widget.isDesktop ? _buildDesktopForm() : _buildMobileForm(),
),
);
}
Widget _buildDesktopForm() {
return Column(
children: [
Expanded(
child: SingleChildScrollView(
child: Column(
children: [
// Selector de empresa
NegocioEmpresaSelector(
provider: widget.provider,
isDesktop: true,
),
const SizedBox(height: 20),
// Campos del formulario en filas para desktop
NegocioFormFields(
isDesktop: true,
nombreController: _nombreController,
direccionController: _direccionController,
latitudController: _latitudController,
longitudController: _longitudController,
tipoLocalController: _tipoLocalController,
),
const SizedBox(height: 25),
// Botones de acción
NegocioActionButtons(
isLoading: _isLoading,
isDesktop: true,
onCancel: () => _handleCancel(),
onSubmit: () => _crearNegocio(),
),
],
),
),
),
],
);
}
Widget _buildMobileForm() {
return SingleChildScrollView(
child: Column(
children: [
// Selector de empresa
NegocioEmpresaSelector(
provider: widget.provider,
isDesktop: false,
),
const SizedBox(height: 20),
// Campos del formulario en columnas para móvil
NegocioFormFields(
isDesktop: false,
nombreController: _nombreController,
direccionController: _direccionController,
latitudController: _latitudController,
longitudController: _longitudController,
tipoLocalController: _tipoLocalController,
),
const SizedBox(height: 25),
// Botones de acción
NegocioActionButtons(
isLoading: _isLoading,
isDesktop: false,
onCancel: () => _handleCancel(),
onSubmit: () => _crearNegocio(),
),
],
),
);
}
void _handleCancel() {
widget.provider.resetFormData();
Navigator.of(context).pop();
}
Future<void> _crearNegocio() async {
if (!_formKey.currentState!.validate()) return;
setState(() {
_isLoading = true;
});
try {
final latitud = double.parse(_latitudController.text.trim());
final longitud = double.parse(_longitudController.text.trim());
final success = await widget.provider.crearNegocio(
empresaId: widget.empresaId, // Usar el empresaId pasado como parámetro
nombre: _nombreController.text.trim(),
direccion: _direccionController.text.trim(),
latitud: latitud,
longitud: longitud,
tipoLocal: _tipoLocalController.text.trim(),
);
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(
'Negocio creado 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 crear el negocio',
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;
});
}
}
}
}

View File

@@ -1,171 +0,0 @@
import 'package:flutter/material.dart';
import 'package:nethive_neo/theme/theme.dart';
class NegocioDialogHeader extends StatelessWidget {
final bool isDesktop;
final Animation<Offset> slideAnimation;
const NegocioDialogHeader({
Key? key,
required this.isDesktop,
required this.slideAnimation,
}) : super(key: key);
@override
Widget build(BuildContext context) {
if (isDesktop) {
return _buildDesktopHeader(context);
} else {
return _buildMobileHeader(context);
}
}
Widget _buildDesktopHeader(BuildContext context) {
return Container(
width: 300,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
AppTheme.of(context).tertiaryColor,
AppTheme.of(context).primaryColor,
AppTheme.of(context).secondaryColor,
],
),
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: [
_buildIcon(context),
const SizedBox(height: 20),
_buildTitle(context, fontSize: 24),
const SizedBox(height: 8),
_buildSubtitle(context, fontSize: 14),
],
),
),
),
);
}
Widget _buildMobileHeader(BuildContext context) {
return 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).tertiaryColor,
AppTheme.of(context).primaryColor,
AppTheme.of(context).secondaryColor,
],
),
boxShadow: [
BoxShadow(
color: AppTheme.of(context).primaryColor.withOpacity(0.5),
blurRadius: 25,
offset: const Offset(0, 15),
spreadRadius: 2,
),
],
),
child: Column(
children: [
_buildIcon(context),
const SizedBox(height: 16),
_buildTitle(context, fontSize: 26),
const SizedBox(height: 8),
_buildSubtitle(context, fontSize: 14, isMobile: true),
],
),
),
);
}
Widget _buildIcon(BuildContext context) {
return Container(
padding: EdgeInsets.all(isDesktop ? 20 : 18),
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: Icon(
Icons.store_rounded,
color: Colors.white,
size: isDesktop ? 35 : 35,
),
);
}
Widget _buildTitle(BuildContext context, {required double fontSize}) {
return Text(
'Nuevo Negocio',
style: TextStyle(
color: Colors.white,
fontSize: fontSize,
fontWeight: FontWeight.bold,
letterSpacing: 1.5,
),
textAlign: TextAlign.center,
);
}
Widget _buildSubtitle(BuildContext context,
{required double fontSize, bool isMobile = false}) {
return 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(
isMobile
? '🏪 Registra un nuevo negocio o sucursal'
: '🏪 Registra un nuevo negocio',
style: TextStyle(
color: Colors.white.withOpacity(0.95),
fontSize: fontSize,
fontWeight: FontWeight.w500,
letterSpacing: 0.5,
),
textAlign: TextAlign.center,
),
);
}
}

View File

@@ -1,192 +0,0 @@
import 'package:flutter/material.dart';
import 'package:nethive_neo/providers/nethive/empresas_negocios_provider.dart';
import 'package:nethive_neo/theme/theme.dart';
class NegocioEmpresaSelector extends StatelessWidget {
final EmpresasNegociosProvider provider;
final bool isDesktop;
const NegocioEmpresaSelector({
Key? key,
required this.provider,
required this.isDesktop,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
AppTheme.of(context).formBackground,
AppTheme.of(context).secondaryBackground.withOpacity(0.8),
],
),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: AppTheme.of(context).primaryColor.withOpacity(0.3),
width: 2,
),
boxShadow: [
BoxShadow(
color: AppTheme.of(context).primaryColor.withOpacity(0.1),
blurRadius: 15,
offset: const Offset(0, 5),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppTheme.of(context).tertiaryColor,
AppTheme.of(context).primaryColor,
],
),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.business_rounded,
color: Colors.white,
size: 20,
),
),
const SizedBox(width: 12),
Text(
'Seleccionar Empresa',
style: TextStyle(
color: AppTheme.of(context).primaryText,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 15),
_buildEmpresaDropdown(context),
],
),
);
}
Widget _buildEmpresaDropdown(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: AppTheme.of(context).secondaryBackground.withOpacity(0.7),
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: AppTheme.of(context).primaryColor.withOpacity(0.3),
width: 2,
),
),
child: DropdownButtonFormField<String>(
value: provider.empresaSeleccionadaId,
decoration: InputDecoration(
hintText: 'Selecciona una empresa',
hintStyle: TextStyle(
color: AppTheme.of(context).secondaryText.withOpacity(0.7),
fontSize: 14,
),
prefixIcon: Container(
margin: const EdgeInsets.all(8),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppTheme.of(context).tertiaryColor,
AppTheme.of(context).primaryColor,
],
),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(
Icons.business_center_rounded,
color: Colors.white,
size: 18,
),
),
border: InputBorder.none,
contentPadding:
const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
),
dropdownColor: AppTheme.of(context).secondaryBackground,
style: TextStyle(
color: AppTheme.of(context).primaryText,
fontSize: 14,
fontWeight: FontWeight.w500,
),
icon: Icon(
Icons.keyboard_arrow_down_rounded,
color: AppTheme.of(context).primaryColor,
size: 28,
),
items: provider.empresas.map((empresa) {
return DropdownMenuItem<String>(
value: empresa.id,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: AppTheme.of(context).primaryColor.withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.business_rounded,
color: AppTheme.of(context).primaryColor,
size: 16,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
empresa.nombre,
style: TextStyle(
color: AppTheme.of(context).primaryText,
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
if (empresa.rfc != null)
Text(
empresa.rfc!,
style: TextStyle(
color: AppTheme.of(context).secondaryText,
fontSize: 12,
),
),
],
),
),
],
),
),
);
}).toList(),
onChanged: (String? newValue) {
provider.setEmpresaSeleccionada(newValue!);
},
validator: (value) {
if (value == null || value.isEmpty) {
return 'Selecciona una empresa';
}
return null;
},
),
);
}
}

View File

@@ -1,350 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:nethive_neo/theme/theme.dart';
class NegocioFormFields extends StatelessWidget {
final bool isDesktop;
final TextEditingController nombreController;
final TextEditingController direccionController;
final TextEditingController latitudController;
final TextEditingController longitudController;
final TextEditingController tipoLocalController;
const NegocioFormFields({
Key? key,
required this.isDesktop,
required this.nombreController,
required this.direccionController,
required this.latitudController,
required this.longitudController,
required this.tipoLocalController,
}) : super(key: key);
@override
Widget build(BuildContext context) {
if (isDesktop) {
return _buildDesktopFields(context);
} else {
return _buildMobileFields(context);
}
}
Widget _buildDesktopFields(BuildContext context) {
return Column(
children: [
// Primera fila - Nombre del negocio
_buildFormField(
context: context,
controller: nombreController,
label: 'Nombre del negocio',
hint: 'Ej: Sucursal Centro, Tienda Principal',
icon: Icons.store_rounded,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'El nombre es requerido';
}
return null;
},
),
const SizedBox(height: 16),
// Segunda fila - Dirección
_buildFormField(
context: context,
controller: direccionController,
label: 'Dirección',
hint: 'Dirección completa del negocio',
icon: Icons.location_on_rounded,
maxLines: 2,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'La dirección es requerida';
}
return null;
},
),
const SizedBox(height: 16),
// Tercera fila - Coordenadas
Row(
children: [
Expanded(
child: _buildFormField(
context: context,
controller: latitudController,
label: 'Latitud',
hint: 'Ej: 19.4326',
icon: Icons.location_searching,
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
signed: true,
),
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'^-?\d*\.?\d*')),
],
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'La latitud es requerida';
}
final lat = double.tryParse(value);
if (lat == null) {
return 'Número inválido';
}
if (lat < -90 || lat > 90) {
return 'Debe estar entre -90 y 90';
}
return null;
},
),
),
const SizedBox(width: 16),
Expanded(
child: _buildFormField(
context: context,
controller: longitudController,
label: 'Longitud',
hint: 'Ej: -99.1332',
icon: Icons.location_searching,
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
signed: true,
),
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'^-?\d*\.?\d*')),
],
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'La longitud es requerida';
}
final lng = double.tryParse(value);
if (lng == null) {
return 'Número inválido';
}
if (lng < -180 || lng > 180) {
return 'Debe estar entre -180 y 180';
}
return null;
},
),
),
],
),
const SizedBox(height: 16),
// Cuarta fila - Tipo de local
_buildFormField(
context: context,
controller: tipoLocalController,
label: 'Tipo de local',
hint: 'Ej: Sucursal, Matriz, Almacén',
icon: Icons.business,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'El tipo de local es requerido';
}
return null;
},
),
],
);
}
Widget _buildMobileFields(BuildContext context) {
return Column(
children: [
_buildFormField(
context: context,
controller: nombreController,
label: 'Nombre del negocio',
hint: 'Ej: Sucursal Centro, Tienda Principal',
icon: Icons.store_rounded,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'El nombre es requerido';
}
return null;
},
),
_buildFormField(
context: context,
controller: direccionController,
label: 'Dirección',
hint: 'Dirección completa del negocio',
icon: Icons.location_on_rounded,
maxLines: 3,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'La dirección es requerida';
}
return null;
},
),
_buildFormField(
context: context,
controller: latitudController,
label: 'Latitud',
hint: 'Ej: 19.4326',
icon: Icons.location_searching,
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
signed: true,
),
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'^-?\d*\.?\d*')),
],
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'La latitud es requerida';
}
final lat = double.tryParse(value);
if (lat == null) {
return 'Número inválido';
}
if (lat < -90 || lat > 90) {
return 'Debe estar entre -90 y 90';
}
return null;
},
),
_buildFormField(
context: context,
controller: longitudController,
label: 'Longitud',
hint: 'Ej: -99.1332',
icon: Icons.location_searching,
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
signed: true,
),
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'^-?\d*\.?\d*')),
],
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'La longitud es requerida';
}
final lng = double.tryParse(value);
if (lng == null) {
return 'Número inválido';
}
if (lng < -180 || lng > 180) {
return 'Debe estar entre -180 y 180';
}
return null;
},
),
_buildFormField(
context: context,
controller: tipoLocalController,
label: 'Tipo de local',
hint: 'Ej: Sucursal, Matriz, Almacén',
icon: Icons.business,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'El tipo de local es requerido';
}
return null;
},
),
],
);
}
Widget _buildFormField({
required BuildContext context,
required TextEditingController controller,
required String label,
required String hint,
required IconData icon,
int maxLines = 1,
TextInputType? keyboardType,
List<TextInputFormatter>? inputFormatters,
String? Function(String?)? validator,
}) {
return Container(
margin: const EdgeInsets.only(bottom: 16),
child: TextFormField(
controller: controller,
maxLines: maxLines,
keyboardType: keyboardType,
inputFormatters: inputFormatters,
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: LinearGradient(
colors: [
AppTheme.of(context).tertiaryColor,
AppTheme.of(context).primaryColor,
],
),
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),
),
),
);
}
}

View File

@@ -1,686 +0,0 @@
import 'package:flutter/material.dart';
import 'package:nethive_neo/providers/nethive/empresas_negocios_provider.dart';
import 'package:nethive_neo/pages/empresa_negocios/widgets/add_empresa_dialog.dart';
import 'package:nethive_neo/pages/empresa_negocios/widgets/add_negocio_dialog.dart';
import 'package:nethive_neo/theme/theme.dart';
import 'package:nethive_neo/helpers/globals.dart';
class EmpresaSelectorSidebar extends StatefulWidget {
final EmpresasNegociosProvider provider;
final Function(String) onEmpresaSelected;
const EmpresaSelectorSidebar({
Key? key,
required this.provider,
required this.onEmpresaSelected,
}) : super(key: key);
@override
State<EmpresaSelectorSidebar> createState() => _EmpresaSelectorSidebarState();
}
class _EmpresaSelectorSidebarState extends State<EmpresaSelectorSidebar>
with TickerProviderStateMixin {
late AnimationController _pulseController;
late Animation<double> _pulseAnimation;
@override
void initState() {
super.initState();
_pulseController = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
)..repeat(reverse: true);
_pulseAnimation = Tween<double>(
begin: 0.8,
end: 1.0,
).animate(CurvedAnimation(
parent: _pulseController,
curve: Curves.easeInOut,
));
}
@override
void dispose() {
_pulseController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
AppTheme.of(context).secondaryBackground,
AppTheme.of(context).primaryBackground,
],
),
border: Border(
right: BorderSide(
color: AppTheme.of(context).primaryColor.withOpacity(0.2),
width: 1,
),
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(2, 0),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header mejorado con gradiente
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: AppTheme.of(context).primaryGradient,
boxShadow: [
BoxShadow(
color: AppTheme.of(context).primaryColor.withOpacity(0.3),
blurRadius: 15,
offset: const Offset(0, 5),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
// Logo animado
AnimatedBuilder(
animation: _pulseAnimation,
builder: (context, child) {
return Transform.scale(
scale: _pulseAnimation.value,
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.white.withOpacity(0.4),
width: 2,
),
),
child: Icon(
Icons.business_center,
color: Colors.white,
size: 24,
),
),
);
},
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ShaderMask(
shaderCallback: (bounds) => LinearGradient(
colors: [
Colors.white,
Colors.white.withOpacity(0.8)
],
).createShader(bounds),
child: Text(
'NETHIVE',
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
letterSpacing: 1.5,
),
),
),
Text(
'Empresas',
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
],
),
),
],
),
const SizedBox(height: 16),
// Contador de empresas con efecto
Container(
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.15),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: Colors.white.withOpacity(0.3),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.domain,
color: Colors.white,
size: 16,
),
const SizedBox(width: 8),
TweenAnimationBuilder<int>(
duration: const Duration(milliseconds: 800),
tween: IntTween(
begin: 0, end: widget.provider.empresas.length),
builder: (context, value, child) {
return Text(
'$value empresas',
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.bold,
),
);
},
),
],
),
),
],
),
),
// Lista de empresas con animaciones escalonadas
Expanded(
child: widget.provider.empresas.isEmpty
? _buildEmptyState()
: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: widget.provider.empresas.length,
itemBuilder: (context, index) {
return TweenAnimationBuilder<double>(
duration: Duration(milliseconds: 200 + (index * 100)),
tween: Tween(begin: 0.0, end: 1.0),
builder: (context, value, child) {
return Transform.translate(
offset: Offset(-50 * (1 - value), 0),
child: Opacity(
opacity: value,
child: _buildEmpresaCard(
widget.provider.empresas[index], index),
),
);
},
);
},
),
),
// Botón añadir empresa mejorado
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
AppTheme.of(context).primaryBackground.withOpacity(0.0),
AppTheme.of(context).primaryBackground,
],
),
),
child: Column(
children: [
// Línea divisoria con gradiente
Container(
height: 1,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.transparent,
AppTheme.of(context).primaryColor.withOpacity(0.5),
Colors.transparent,
],
),
),
),
const SizedBox(height: 20),
// Botón principal
SizedBox(
width: double.infinity,
child: Container(
decoration: BoxDecoration(
gradient: AppTheme.of(context).modernGradient,
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: AppTheme.of(context)
.primaryColor
.withOpacity(0.3),
blurRadius: 15,
offset: const Offset(0, 5),
),
],
),
child: ElevatedButton.icon(
onPressed: () {
showDialog(
context: context,
builder: (context) =>
AddEmpresaDialog(provider: widget.provider),
);
},
icon: const Icon(Icons.add, color: Colors.white),
label: const Text(
'Nueva Empresa',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
),
),
),
),
],
),
),
// Información de empresa seleccionada mejorada
if (widget.provider.empresaSeleccionada != null) ...[
Container(
margin: const EdgeInsets.fromLTRB(16, 0, 16, 16),
padding: const EdgeInsets.all(20),
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),
],
),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: AppTheme.of(context).primaryColor.withOpacity(0.3),
width: 2,
),
boxShadow: [
BoxShadow(
color: AppTheme.of(context).primaryColor.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
gradient: AppTheme.of(context).primaryGradient,
borderRadius: BorderRadius.circular(10),
),
child: Icon(
Icons.check_circle,
color: Colors.white,
size: 20,
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
'Empresa activa',
style: TextStyle(
color: AppTheme.of(context).primaryColor,
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
),
],
),
const SizedBox(height: 12),
Text(
widget.provider.empresaSeleccionada!.nombre,
style: TextStyle(
color: AppTheme.of(context).primaryText,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
// Estadísticas
Row(
children: [
Icon(
Icons.store,
size: 16,
color: AppTheme.of(context).primaryColor,
),
const SizedBox(width: 6),
TweenAnimationBuilder<int>(
duration: const Duration(milliseconds: 600),
tween: IntTween(
begin: 0, end: widget.provider.negocios.length),
builder: (context, value, child) {
return Text(
'$value sucursales',
style: TextStyle(
color: AppTheme.of(context).primaryText,
fontSize: 14,
fontWeight: FontWeight.w600,
),
);
},
),
],
),
const SizedBox(height: 16),
// Botón para añadir sucursal
SizedBox(
width: double.infinity,
child: Container(
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: ElevatedButton.icon(
onPressed: () {
showDialog(
context: context,
builder: (context) => AddNegocioDialog(
provider: widget.provider,
empresaId:
widget.provider.empresaSeleccionada!.id,
),
);
},
icon: const Icon(Icons.add_location,
size: 18, color: Colors.white),
label: const Text(
'Añadir Sucursal',
style: TextStyle(
fontSize: 14,
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
),
),
),
],
),
),
],
],
),
);
}
Widget _buildEmpresaCard(dynamic empresa, int index) {
final isSelected = widget.provider.empresaSeleccionadaId == empresa.id;
return Container(
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
gradient: isSelected
? AppTheme.of(context).primaryGradient
: LinearGradient(
colors: [
AppTheme.of(context).secondaryBackground,
AppTheme.of(context).tertiaryBackground,
],
),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isSelected
? Colors.white.withOpacity(0.3)
: AppTheme.of(context).primaryColor.withOpacity(0.2),
width: isSelected ? 2 : 1,
),
boxShadow: [
BoxShadow(
color: isSelected
? AppTheme.of(context).primaryColor.withOpacity(0.3)
: Colors.black.withOpacity(0.1),
blurRadius: isSelected ? 15 : 8,
offset: Offset(0, isSelected ? 8 : 3),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => widget.onEmpresaSelected(empresa.id),
borderRadius: BorderRadius.circular(16),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
// Logo de la empresa con efectos
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
gradient: isSelected
? LinearGradient(
colors: [
Colors.white.withOpacity(0.3),
Colors.white.withOpacity(0.1)
],
)
: AppTheme.of(context).primaryGradient,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: (isSelected
? Colors.white
: AppTheme.of(context).primaryColor)
.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 3),
),
],
),
child: empresa.logoUrl != null &&
empresa.logoUrl!.isNotEmpty
? ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(
"${supabaseLU.supabaseUrl}/storage/v1/object/public/nethive/logos/${empresa.logoUrl}",
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Image.asset(
'assets/images/placeholder_no_image.jpg',
fit: BoxFit.cover,
);
},
),
)
: Icon(
Icons.business,
color: isSelected ? Colors.white : Colors.white,
size: 24,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
empresa.nombre,
style: TextStyle(
color: isSelected
? Colors.white
: AppTheme.of(context).primaryText,
fontSize: 16,
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: (isSelected
? Colors.white
: AppTheme.of(context).primaryColor)
.withOpacity(0.2),
borderRadius: BorderRadius.circular(10),
),
child: Text(
'Tecnología',
style: TextStyle(
color: isSelected
? Colors.white
: AppTheme.of(context).primaryColor,
fontSize: 10,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
],
),
const SizedBox(height: 12),
// Información adicional
Row(
children: [
Icon(
Icons.store,
size: 16,
color: isSelected
? Colors.white.withOpacity(0.8)
: AppTheme.of(context).secondaryText,
),
const SizedBox(width: 6),
Text(
'Sucursales: ${isSelected ? widget.provider.negocios.length : '...'}',
style: TextStyle(
color: isSelected
? Colors.white.withOpacity(0.9)
: AppTheme.of(context).secondaryText,
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
],
),
],
),
),
),
),
);
}
Widget _buildEmptyState() {
return Center(
child: Padding(
padding: const EdgeInsets.all(40),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedBuilder(
animation: _pulseAnimation,
builder: (context, child) {
return Transform.scale(
scale: _pulseAnimation.value,
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: AppTheme.of(context).primaryGradient,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: AppTheme.of(context)
.primaryColor
.withOpacity(0.3),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
),
child: Icon(
Icons.business,
color: Colors.white,
size: 40,
),
),
);
},
),
const SizedBox(height: 20),
Text(
'Sin empresas',
style: TextStyle(
color: AppTheme.of(context).primaryText,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
'Añade tu primera empresa\npara comenzar',
textAlign: TextAlign.center,
style: TextStyle(
color: AppTheme.of(context).secondaryText,
fontSize: 14,
),
),
],
),
),
);
}
}

View File

@@ -1,472 +0,0 @@
import 'package:flutter/material.dart';
import 'package:nethive_neo/providers/nethive/empresas_negocios_provider.dart';
import 'package:nethive_neo/theme/theme.dart';
import 'package:nethive_neo/helpers/globals.dart';
class MobileEmpresaSelector extends StatefulWidget {
final EmpresasNegociosProvider provider;
final Function(String) onEmpresaSelected;
const MobileEmpresaSelector({
Key? key,
required this.provider,
required this.onEmpresaSelected,
}) : super(key: key);
@override
State<MobileEmpresaSelector> createState() => _MobileEmpresaSelectorState();
}
class _MobileEmpresaSelectorState extends State<MobileEmpresaSelector>
with TickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
final TextEditingController _searchController = TextEditingController();
List<dynamic> _filteredEmpresas = [];
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_fadeAnimation = CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
);
_filteredEmpresas = widget.provider.empresas;
_animationController.forward();
_searchController.addListener(_onSearchChanged);
}
@override
void dispose() {
_animationController.dispose();
_searchController.dispose();
super.dispose();
}
void _onSearchChanged() {
final query = _searchController.text.toLowerCase();
setState(() {
_filteredEmpresas = widget.provider.empresas.where((empresa) {
return empresa.nombre.toLowerCase().contains(query) ||
empresa.rfc.toLowerCase().contains(query);
}).toList();
});
}
@override
Widget build(BuildContext context) {
return FadeTransition(
opacity: _fadeAnimation,
child: Container(
decoration: BoxDecoration(
color: AppTheme.of(context).primaryBackground,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(25),
topRight: Radius.circular(25),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Handle
Container(
margin: const EdgeInsets.symmetric(vertical: 10),
width: 40,
height: 4,
decoration: BoxDecoration(
color: AppTheme.of(context).secondaryText.withOpacity(0.3),
borderRadius: BorderRadius.circular(2),
),
),
// Header
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: AppTheme.of(context).primaryGradient,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(25),
topRight: Radius.circular(25),
),
),
child: Column(
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.business_center,
color: Colors.white,
size: 24,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Seleccionar Empresa',
style: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
Text(
'Elige una empresa para ver sus sucursales',
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 14,
),
),
],
),
),
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(
Icons.close,
color: Colors.white,
),
),
],
),
const SizedBox(height: 16),
// Buscador
Container(
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(15),
),
child: TextField(
controller: _searchController,
style: const TextStyle(color: Colors.white),
decoration: InputDecoration(
hintText: 'Buscar empresa...',
hintStyle: TextStyle(
color: Colors.white.withOpacity(0.7),
),
prefixIcon: Icon(
Icons.search,
color: Colors.white.withOpacity(0.7),
),
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 15,
),
),
),
),
],
),
),
// Lista de empresas
Flexible(
child: _filteredEmpresas.isEmpty
? _buildEmptyState()
: ListView.builder(
padding: const EdgeInsets.all(16),
shrinkWrap: true,
itemCount: _filteredEmpresas.length,
itemBuilder: (context, index) {
return TweenAnimationBuilder<double>(
duration: Duration(milliseconds: 200 + (index * 50)),
tween: Tween(begin: 0.0, end: 1.0),
builder: (context, value, child) {
return Transform.translate(
offset: Offset(30 * (1 - value), 0),
child: Opacity(
opacity: value,
child: _buildEmpresaCard(
_filteredEmpresas[index],
index,
),
),
);
},
);
},
),
),
// Footer con información adicional
if (widget.provider.empresaSeleccionada != null) ...[
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppTheme.of(context).secondaryBackground,
border: Border(
top: BorderSide(
color: AppTheme.of(context).primaryColor.withOpacity(0.2),
),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Empresa Actual',
style: TextStyle(
color: AppTheme.of(context).primaryColor,
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
widget.provider.empresaSeleccionada!.nombre,
style: TextStyle(
color: AppTheme.of(context).primaryText,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
'${widget.provider.negocios.length} sucursales',
style: TextStyle(
color: AppTheme.of(context).secondaryText,
fontSize: 14,
),
),
],
),
),
],
],
),
),
);
}
Widget _buildEmpresaCard(dynamic empresa, int index) {
final isSelected = widget.provider.empresaSeleccionadaId == empresa.id;
return Container(
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
gradient: isSelected
? AppTheme.of(context).primaryGradient
: LinearGradient(
colors: [
AppTheme.of(context).secondaryBackground,
AppTheme.of(context).tertiaryBackground,
],
),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isSelected
? Colors.white.withOpacity(0.3)
: AppTheme.of(context).primaryColor.withOpacity(0.2),
width: isSelected ? 2 : 1,
),
boxShadow: [
BoxShadow(
color: isSelected
? AppTheme.of(context).primaryColor.withOpacity(0.3)
: Colors.black.withOpacity(0.1),
blurRadius: isSelected ? 15 : 8,
offset: Offset(0, isSelected ? 8 : 3),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
widget.onEmpresaSelected(empresa.id);
Navigator.of(context).pop();
},
borderRadius: BorderRadius.circular(16),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
// Logo de la empresa
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
gradient: isSelected
? LinearGradient(
colors: [
Colors.white.withOpacity(0.3),
Colors.white.withOpacity(0.1)
],
)
: AppTheme.of(context).primaryGradient,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: (isSelected
? Colors.white
: AppTheme.of(context).primaryColor)
.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 3),
),
],
),
child: empresa.logoUrl != null && empresa.logoUrl!.isNotEmpty
? ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(
"${supabaseLU.supabaseUrl}/storage/v1/object/public/nethive/logos/${empresa.logoUrl}",
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Image.asset(
'assets/images/placeholder_no_image.jpg',
fit: BoxFit.cover,
);
},
),
)
: Icon(
Icons.business,
color: isSelected ? Colors.white : Colors.white,
size: 30,
),
),
const SizedBox(width: 16),
// Información de la empresa
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
empresa.nombre,
style: TextStyle(
color: isSelected
? Colors.white
: AppTheme.of(context).primaryText,
fontSize: 16,
fontWeight: FontWeight.bold,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
empresa.rfc,
style: TextStyle(
color: isSelected
? Colors.white.withOpacity(0.8)
: AppTheme.of(context).secondaryText,
fontSize: 14,
),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: (isSelected
? Colors.white
: AppTheme.of(context).primaryColor)
.withOpacity(0.2),
borderRadius: BorderRadius.circular(10),
),
child: Text(
'Tecnología',
style: TextStyle(
color: isSelected
? Colors.white
: AppTheme.of(context).primaryColor,
fontSize: 10,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
// Indicador de selección
if (isSelected)
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
shape: BoxShape.circle,
),
child: const Icon(
Icons.check,
color: Colors.white,
size: 20,
),
)
else
Icon(
Icons.arrow_forward_ios,
color: AppTheme.of(context).secondaryText,
size: 16,
),
],
),
),
),
),
);
}
Widget _buildEmptyState() {
return Center(
child: Padding(
padding: const EdgeInsets.all(40),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppTheme.of(context).primaryColor.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
Icons.search_off,
color: AppTheme.of(context).primaryColor,
size: 40,
),
),
const SizedBox(height: 16),
Text(
'No se encontraron empresas',
style: TextStyle(
color: AppTheme.of(context).primaryText,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
'Intenta con otro término de búsqueda',
textAlign: TextAlign.center,
style: TextStyle(
color: AppTheme.of(context).secondaryText,
fontSize: 14,
),
),
],
),
),
);
}
}

View File

@@ -1,715 +0,0 @@
import 'package:flutter/material.dart';
import 'package:nethive_neo/providers/nethive/empresas_negocios_provider.dart';
import 'package:nethive_neo/theme/theme.dart';
import 'package:nethive_neo/helpers/globals.dart';
class NegociosCardsView extends StatelessWidget {
final EmpresasNegociosProvider provider;
const NegociosCardsView({
Key? key,
required this.provider,
}) : super(key: key);
@override
Widget build(BuildContext context) {
if (provider.empresaSeleccionada == null) {
return _buildEmptyState(context);
}
if (provider.negocios.isEmpty) {
return _buildNoDataState(context);
}
return Container(
padding: const EdgeInsets.all(16),
child: ListView.builder(
itemCount: provider.negocios.length,
itemBuilder: (context, index) {
final negocio = provider.negocios[index];
return TweenAnimationBuilder<double>(
duration: Duration(milliseconds: 300 + (index * 100)),
tween: Tween(begin: 0.0, end: 1.0),
builder: (context, value, child) {
return Transform.translate(
offset: Offset(0, 50 * (1 - value)),
child: Opacity(
opacity: value,
child: Container(
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
AppTheme.of(context).secondaryBackground,
AppTheme.of(context).tertiaryBackground,
],
),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color:
AppTheme.of(context).primaryColor.withOpacity(0.3),
width: 1,
),
boxShadow: [
BoxShadow(
color: AppTheme.of(context)
.primaryColor
.withOpacity(0.1),
blurRadius: 15,
offset: const Offset(0, 5),
),
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
_showNegocioDetails(context, negocio);
},
borderRadius: BorderRadius.circular(20),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header con logo y nombre
Row(
children: [
// Logo del negocio
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
gradient: AppTheme.of(context)
.primaryGradient,
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: AppTheme.of(context)
.primaryColor
.withOpacity(0.3),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: negocio.logoUrl != null &&
negocio.logoUrl!.isNotEmpty
? ClipRRect(
borderRadius:
BorderRadius.circular(15),
child: Image.network(
"${supabaseLU.supabaseUrl}/storage/v1/object/public/nethive/logos/${negocio.logoUrl}",
fit: BoxFit.cover,
errorBuilder: (context, error,
stackTrace) {
return Image.asset(
'assets/images/placeholder_no_image.jpg',
fit: BoxFit.cover,
);
},
),
)
: Icon(
Icons.store,
color: Colors.white,
size: 30,
),
),
const SizedBox(width: 16),
// Información principal
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
negocio.nombre,
style: TextStyle(
color: AppTheme.of(context)
.primaryText,
fontSize: 20,
fontWeight: FontWeight.bold,
letterSpacing: 0.5,
),
),
const SizedBox(height: 4),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 4,
),
decoration: BoxDecoration(
color: AppTheme.of(context)
.primaryColor
.withOpacity(0.2),
borderRadius:
BorderRadius.circular(15),
border: Border.all(
color: AppTheme.of(context)
.primaryColor
.withOpacity(0.4),
),
),
child: Text(
negocio.tipoLocal.isNotEmpty
? negocio.tipoLocal
: 'Sucursal',
style: TextStyle(
color: AppTheme.of(context)
.primaryColor,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
// Botón de acciones
PopupMenuButton<String>(
icon: Icon(
Icons.more_vert,
color:
AppTheme.of(context).secondaryText,
),
color: AppTheme.of(context)
.secondaryBackground,
onSelected: (value) {
_handleMenuAction(
context, value, negocio);
},
itemBuilder: (context) => [
PopupMenuItem(
value: 'edit',
child: Row(
children: [
Icon(Icons.edit,
color: AppTheme.of(context)
.primaryColor),
const SizedBox(width: 8),
Text(
'Editar',
style: TextStyle(
color: AppTheme.of(context)
.primaryText),
),
],
),
),
PopupMenuItem(
value: 'components',
child: Row(
children: [
Icon(Icons.inventory_2,
color: Colors.orange),
const SizedBox(width: 8),
Text(
'Ver Componentes',
style: TextStyle(
color: AppTheme.of(context)
.primaryText),
),
],
),
),
PopupMenuItem(
value: 'delete',
child: Row(
children: [
Icon(Icons.delete,
color: Colors.red),
const SizedBox(width: 8),
Text(
'Eliminar',
style: TextStyle(
color: AppTheme.of(context)
.primaryText),
),
],
),
),
],
),
],
),
const SizedBox(height: 16),
// Información de ubicación
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppTheme.of(context)
.primaryBackground
.withOpacity(0.5),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppTheme.of(context)
.primaryColor
.withOpacity(0.2),
),
),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.location_on,
color: AppTheme.of(context)
.primaryColor,
size: 18,
),
const SizedBox(width: 8),
Text(
'Ubicación',
style: TextStyle(
color: AppTheme.of(context)
.primaryColor,
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 8),
Text(
negocio.direccion.isNotEmpty
? negocio.direccion
: 'Sin dirección',
style: TextStyle(
color:
AppTheme.of(context).primaryText,
fontSize: 14,
height: 1.4,
),
),
],
),
),
const SizedBox(height: 12),
// Coordenadas y estadísticas
Row(
children: [
Expanded(
child: _buildInfoChip(
context,
icon: Icons.gps_fixed,
label: 'Coordenadas',
value:
'${negocio.latitud.toStringAsFixed(4)}, ${negocio.longitud.toStringAsFixed(4)}',
),
),
const SizedBox(width: 12),
Expanded(
child: _buildInfoChip(
context,
icon: Icons.people,
label: 'Empleados',
value: negocio.tipoLocal == 'Sucursal'
? '95'
: '120',
),
),
],
),
const SizedBox(height: 12),
// Fecha de creación
Row(
children: [
Icon(
Icons.calendar_today,
color: AppTheme.of(context).secondaryText,
size: 16,
),
const SizedBox(width: 8),
Text(
'Creado: ${negocio.fechaCreacion.toString().split(' ')[0]}',
style: TextStyle(
color:
AppTheme.of(context).secondaryText,
fontSize: 12,
),
),
],
),
],
),
),
),
),
),
),
),
);
},
);
},
),
);
}
Widget _buildInfoChip(
BuildContext context, {
required IconData icon,
required String label,
required String value,
}) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppTheme.of(context).primaryColor.withOpacity(0.1),
AppTheme.of(context).tertiaryColor.withOpacity(0.1),
],
),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppTheme.of(context).primaryColor.withOpacity(0.3),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
icon,
color: AppTheme.of(context).primaryColor,
size: 16,
),
const SizedBox(width: 6),
Text(
label,
style: TextStyle(
color: AppTheme.of(context).primaryColor,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 4),
Text(
value,
style: TextStyle(
color: AppTheme.of(context).primaryText,
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
Widget _buildEmptyState(BuildContext context) {
return Center(
child: Container(
padding: const EdgeInsets.all(40),
margin: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: AppTheme.of(context).primaryGradient,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: AppTheme.of(context).primaryColor.withOpacity(0.3),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.business_center,
size: 60,
color: Colors.white,
),
const SizedBox(height: 16),
Text(
'Selecciona una empresa',
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
'Toca el botón de empresas para seleccionar una y ver sus sucursales',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 14,
),
),
],
),
),
);
}
Widget _buildNoDataState(BuildContext context) {
return Center(
child: Container(
padding: const EdgeInsets.all(40),
margin: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppTheme.of(context).secondaryBackground,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: AppTheme.of(context).primaryColor.withOpacity(0.3),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.store_mall_directory,
size: 60,
color: AppTheme.of(context).secondaryText,
),
const SizedBox(height: 16),
Text(
'Sin sucursales',
style: TextStyle(
color: AppTheme.of(context).primaryText,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
'Esta empresa aún no tiene sucursales registradas',
textAlign: TextAlign.center,
style: TextStyle(
color: AppTheme.of(context).secondaryText,
fontSize: 14,
),
),
],
),
),
);
}
void _showNegocioDetails(BuildContext context, dynamic negocio) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => DraggableScrollableSheet(
initialChildSize: 0.6,
maxChildSize: 0.9,
minChildSize: 0.3,
builder: (context, scrollController) => Container(
decoration: BoxDecoration(
color: AppTheme.of(context).primaryBackground,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(25),
topRight: Radius.circular(25),
),
),
child: Column(
children: [
// Handle
Container(
margin: const EdgeInsets.symmetric(vertical: 10),
width: 40,
height: 4,
decoration: BoxDecoration(
color: AppTheme.of(context).secondaryText,
borderRadius: BorderRadius.circular(2),
),
),
// Contenido del modal
Expanded(
child: SingleChildScrollView(
controller: scrollController,
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Detalles de la Sucursal',
style: TextStyle(
color: AppTheme.of(context).primaryText,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 20),
// Aquí puedes agregar más detalles específicos
Text(
'Información adicional de ${negocio.nombre}',
style: TextStyle(
color: AppTheme.of(context).secondaryText,
fontSize: 16,
),
),
],
),
),
),
],
),
),
),
);
}
void _handleMenuAction(BuildContext context, String action, dynamic negocio) {
switch (action) {
case 'edit':
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Función de edición próximamente')),
);
break;
case 'components':
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Ver componentes de ${negocio.nombre}')),
);
break;
case 'delete':
_showDeleteDialog(context, negocio);
break;
}
}
void _showDeleteDialog(BuildContext context, dynamic negocio) {
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: AppTheme.of(context).primaryBackground,
title: Text(
'Confirmar eliminación',
style: TextStyle(color: AppTheme.of(context).primaryText),
),
content: Text(
'¿Estás seguro de que deseas eliminar la sucursal "${negocio.nombre}"?',
style: TextStyle(color: AppTheme.of(context).secondaryText),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
'Cancelar',
style: TextStyle(color: AppTheme.of(context).secondaryText),
),
),
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: 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',
style: TextStyle(color: Colors.red),
),
),
],
),
);
}
}

View File

@@ -1,825 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import 'package:go_router/go_router.dart';
import 'package:nethive_neo/providers/nethive/empresas_negocios_provider.dart';
import 'package:nethive_neo/models/nethive/negocio_model.dart';
import 'package:nethive_neo/theme/theme.dart';
import 'package:nethive_neo/helpers/globals.dart';
class NegociosMapView extends StatefulWidget {
final EmpresasNegociosProvider provider;
const NegociosMapView({
Key? key,
required this.provider,
}) : super(key: key);
@override
State<NegociosMapView> createState() => _NegociosMapViewState();
}
class _NegociosMapViewState extends State<NegociosMapView>
with TickerProviderStateMixin {
final MapController _mapController = MapController();
late AnimationController _markerAnimationController;
late AnimationController _tooltipAnimationController;
late Animation<double> _markerAnimation;
late Animation<double> _tooltipAnimation;
late Animation<Offset> _tooltipSlideAnimation;
String? _hoveredNegocioId;
Offset? _tooltipPosition;
bool _showTooltip = false;
@override
void initState() {
super.initState();
_markerAnimationController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_tooltipAnimationController = AnimationController(
duration: const Duration(milliseconds: 200),
vsync: this,
);
_markerAnimation = Tween<double>(
begin: 1.0,
end: 1.4,
).animate(CurvedAnimation(
parent: _markerAnimationController,
curve: Curves.easeOutBack,
));
_tooltipAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _tooltipAnimationController,
curve: Curves.easeOutCubic,
));
_tooltipSlideAnimation = Tween<Offset>(
begin: const Offset(0, 0.5),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _tooltipAnimationController,
curve: Curves.easeOutCubic,
));
// Centrar el mapa después de que se construya
WidgetsBinding.instance.addPostFrameCallback((_) {
_centerMapOnNegocios();
});
}
@override
void dispose() {
_markerAnimationController.dispose();
_tooltipAnimationController.dispose();
super.dispose();
}
void _showTooltipForNegocio(String negocioId, Offset position) {
setState(() {
_hoveredNegocioId = negocioId;
_tooltipPosition = Offset(
position.dx - 300, // Más cerca del cursor
position.dy - 500, // Más cerca del cursor
);
_showTooltip = true;
});
_markerAnimationController.forward();
_tooltipAnimationController.forward();
}
void _hideTooltip() {
_tooltipAnimationController.reverse().then((_) {
if (mounted) {
setState(() {
_hoveredNegocioId = null;
_showTooltip = false;
_tooltipPosition = null;
});
}
});
_markerAnimationController.reverse();
}
void _centerMapOnNegocios() {
if (widget.provider.negocios.isNotEmpty) {
final bounds = _calculateBounds();
_mapController.fitCamera(
CameraFit.bounds(
bounds: bounds,
padding: const EdgeInsets.all(50),
maxZoom: 15,
),
);
}
}
LatLngBounds _calculateBounds() {
if (widget.provider.negocios.isEmpty) {
return LatLngBounds(
const LatLng(-90, -180),
const LatLng(90, 180),
);
}
double minLat = widget.provider.negocios.first.latitud;
double maxLat = widget.provider.negocios.first.latitud;
double minLng = widget.provider.negocios.first.longitud;
double maxLng = widget.provider.negocios.first.longitud;
for (final negocio in widget.provider.negocios) {
minLat = minLat < negocio.latitud ? minLat : negocio.latitud;
maxLat = maxLat > negocio.latitud ? maxLat : negocio.latitud;
minLng = minLng < negocio.longitud ? minLng : negocio.longitud;
maxLng = maxLng > negocio.longitud ? maxLng : negocio.longitud;
}
return LatLngBounds(
LatLng(minLat, minLng),
LatLng(maxLat, maxLng),
);
}
@override
Widget build(BuildContext context) {
if (widget.provider.negocios.isEmpty) {
return _buildEmptyMapState();
}
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 15,
offset: const Offset(0, 5),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Stack(
children: [
// Listener para detectar movimientos del mouse sobre el mapa
MouseRegion(
onExit: (_) => _hideTooltip(),
child: FlutterMap(
mapController: _mapController,
options: MapOptions(
initialCenter: const LatLng(19.4326, -99.1332),
initialZoom: 10,
minZoom: 3,
maxZoom: 18,
),
children: [
// Capa de tiles del mapa
TileLayer(
urlTemplate:
'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'com.nethive.app',
),
// Capa de marcadores
MarkerLayer(
markers: _buildMarkers(),
),
],
),
),
// Header del mapa con información
Positioned(
top: 20,
left: 20,
right: 20,
child: _buildMapHeader(),
),
// Controles del mapa
Positioned(
bottom: 20,
right: 20,
child: _buildMapControls(),
),
// Tooltip flotante con información del negocio
if (_showTooltip && _tooltipPosition != null)
Positioned(
left: _tooltipPosition!.dx
.clamp(20.0, MediaQuery.of(context).size.width - 280),
top: _tooltipPosition!.dy
.clamp(20.0, MediaQuery.of(context).size.height - 150),
child: _buildAnimatedTooltip(),
),
],
),
),
);
}
List<Marker> _buildMarkers() {
return widget.provider.negocios.map((negocio) {
final isHovered = _hoveredNegocioId == negocio.id;
return Marker(
point: LatLng(negocio.latitud, negocio.longitud),
width: 50,
height: 50,
child: MouseRegion(
cursor: SystemMouseCursors.click, // Cursor de puntero
onEnter: (event) {
_showTooltipForNegocio(negocio.id, event.position);
},
onHover: (event) {
// Actualizar posición del tooltip sin recalcular todo
if (_hoveredNegocioId == negocio.id) {
setState(() {
_tooltipPosition = Offset(
event.position.dx - 300, // Más cerca del cursor
event.position.dy - 400, // Más cerca del cursor
);
});
}
},
child: GestureDetector(
onTap: () {
// Navegar a la infraestructura del negocio
context.go('/infrastructure/${negocio.id}');
},
child: AnimatedBuilder(
animation: _markerAnimation,
builder: (context, child) {
return Transform.scale(
scale: isHovered ? _markerAnimation.value : 1.0,
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: isHovered
? AppTheme.of(context).primaryGradient
: AppTheme.of(context).modernGradient,
boxShadow: [
BoxShadow(
color: AppTheme.of(context)
.primaryColor
.withOpacity(0.4),
blurRadius: isHovered ? 15 : 8,
offset: const Offset(0, 4),
),
],
border: Border.all(
color: Colors.white,
width: isHovered ? 3 : 2,
),
),
child: ClipOval(
child: _buildMarkerContent(negocio, isHovered),
),
),
);
},
),
),
),
);
}).toList();
}
Widget _buildMarkerContent(Negocio negocio, bool isHovered) {
// Verificar si tiene imagen válida
if (negocio.imagenUrl != null && negocio.imagenUrl!.isNotEmpty) {
final imageUrl =
"${supabaseLU.supabaseUrl}/storage/v1/object/public/nethive/imagenes/${negocio.imagenUrl}";
return Image.network(
imageUrl,
fit: BoxFit.cover,
width: 40,
height: 40,
errorBuilder: (context, error, stackTrace) {
// Si falla la imagen, mostrar el ícono
return _buildMarkerIcon(isHovered);
},
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) {
return child;
}
// Mientras carga, mostrar el ícono
return _buildMarkerIcon(isHovered);
},
);
} else {
// Si no hay imagen, mostrar el ícono
return _buildMarkerIcon(isHovered);
}
}
Widget _buildMarkerIcon(bool isHovered) {
return Container(
width: 40,
height: 40,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.transparent,
),
child: Icon(
Icons.store,
color: Colors.white,
size: isHovered ? 22 : 18,
),
);
}
Widget _buildAnimatedTooltip() {
final negocio = widget.provider.negocios.firstWhere(
(n) => n.id == _hoveredNegocioId,
);
return SlideTransition(
position: _tooltipSlideAnimation,
child: FadeTransition(
opacity: _tooltipAnimation,
child: ScaleTransition(
scale: _tooltipAnimation,
child: Container(
width: 260,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: AppTheme.of(context).modernGradient,
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: AppTheme.of(context).primaryColor.withOpacity(0.3),
blurRadius: 20,
offset: const Offset(0, 8),
),
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// Header con imagen y nombre
Row(
children: [
// Imagen del negocio o ícono por defecto
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: Colors.white.withOpacity(0.3),
width: 2,
),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: _buildTooltipImage(negocio),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
negocio.nombre,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(10),
),
child: Text(
negocio.tipoLocal,
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
],
),
const SizedBox(height: 12),
// Información de ubicación
Row(
children: [
Icon(
Icons.location_on,
color: Colors.white.withOpacity(0.8),
size: 16,
),
const SizedBox(width: 6),
Expanded(
child: Text(
negocio.direccion,
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 14,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 8),
// Coordenadas
Row(
children: [
Icon(
Icons.my_location,
color: Colors.white.withOpacity(0.8),
size: 16,
),
const SizedBox(width: 6),
Text(
'Lat: ${negocio.latitud.toStringAsFixed(4)}, Lng: ${negocio.longitud.toStringAsFixed(4)}',
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 12,
fontFamily: 'monospace',
),
),
],
),
const SizedBox(height: 12),
// Call to action
Container(
width: double.infinity,
padding:
const EdgeInsets.symmetric(horizontal: 12, 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(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.touch_app,
color: Colors.white,
size: 16,
),
const SizedBox(width: 8),
Text(
'Clic para ver infraestructura',
style: TextStyle(
color: Colors.white,
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
],
),
),
],
),
),
),
),
);
}
Widget _buildTooltipImage(Negocio negocio) {
// Verificar si tiene imagen válida
if (negocio.imagenUrl != null && negocio.imagenUrl!.isNotEmpty) {
final imageUrl =
"${supabaseLU.supabaseUrl}/storage/v1/object/public/nethive/imagenes/${negocio.imagenUrl}";
return Image.network(
imageUrl,
fit: BoxFit.cover,
width: 50,
height: 50,
errorBuilder: (context, error, stackTrace) {
// Si falla la imagen, mostrar el ícono
return _buildTooltipIcon();
},
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) {
return child;
}
// Mientras carga, mostrar un spinner
return Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
),
);
},
);
} else {
// Si no hay imagen, mostrar el ícono
return _buildTooltipIcon();
}
}
Widget _buildTooltipIcon() {
return Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.store,
color: Colors.white,
size: 24,
),
);
}
Widget _buildMapHeader() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: AppTheme.of(context).primaryGradient,
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: AppTheme.of(context).primaryColor.withOpacity(0.3),
blurRadius: 15,
offset: const Offset(0, 5),
),
],
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(10),
),
child: Icon(
Icons.map,
color: Colors.white,
size: 20,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Mapa de Sucursales',
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text(
'${widget.provider.negocios.length} ubicaciones de ${widget.provider.empresaSeleccionada?.nombre ?? ""}',
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 14,
),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.location_on,
color: Colors.white,
size: 16,
),
const SizedBox(width: 6),
Text(
'OpenStreetMap',
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
],
),
),
],
),
);
}
Widget _buildMapControls() {
return Column(
children: [
// Botón de centrar mapa
Container(
decoration: BoxDecoration(
gradient: AppTheme.of(context).primaryGradient,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: AppTheme.of(context).primaryColor.withOpacity(0.3),
blurRadius: 10,
offset: const Offset(0, 3),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: _centerMapOnNegocios,
borderRadius: BorderRadius.circular(10),
child: Container(
padding: const EdgeInsets.all(12),
child: Icon(
Icons.center_focus_strong,
color: Colors.white,
size: 24,
),
),
),
),
),
const SizedBox(height: 12),
// Botón de zoom in
Container(
decoration: BoxDecoration(
color: AppTheme.of(context).secondaryBackground,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
_mapController.move(
_mapController.camera.center,
_mapController.camera.zoom + 1,
);
},
borderRadius: BorderRadius.circular(10),
child: Container(
padding: const EdgeInsets.all(12),
child: Icon(
Icons.add,
color: AppTheme.of(context).primaryColor,
size: 20,
),
),
),
),
),
const SizedBox(height: 8),
// Botón de zoom out
Container(
decoration: BoxDecoration(
color: AppTheme.of(context).secondaryBackground,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
_mapController.move(
_mapController.camera.center,
_mapController.camera.zoom - 1,
);
},
borderRadius: BorderRadius.circular(10),
child: Container(
padding: const EdgeInsets.all(12),
child: Icon(
Icons.remove,
color: AppTheme.of(context).primaryColor,
size: 20,
),
),
),
),
),
],
);
}
Widget _buildEmptyMapState() {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.blue.withOpacity(0.1),
Colors.blue.withOpacity(0.3),
AppTheme.of(context).primaryColor.withOpacity(0.2),
],
),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: AppTheme.of(context).primaryColor,
width: 2,
),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: AppTheme.of(context).modernGradient,
borderRadius: BorderRadius.circular(20),
),
child: Icon(
Icons.location_off,
size: 60,
color: Colors.white,
),
),
const SizedBox(height: 20),
Text(
'Sin ubicaciones para mostrar',
style: TextStyle(
color: AppTheme.of(context).primaryColor,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
Text(
'Selecciona una empresa con sucursales\npara ver sus ubicaciones en el mapa',
textAlign: TextAlign.center,
style: TextStyle(
color: AppTheme.of(context).secondaryText,
fontSize: 16,
),
),
],
),
),
);
}
bool get isHovered => _hoveredNegocioId != null;
}

View File

@@ -1,619 +0,0 @@
import 'package:flutter/material.dart';
import 'package:pluto_grid/pluto_grid.dart';
import 'package:go_router/go_router.dart';
import 'package:nethive_neo/providers/nethive/empresas_negocios_provider.dart';
import 'package:nethive_neo/pages/widgets/animated_hover_button.dart';
import 'package:nethive_neo/theme/theme.dart';
import 'package:provider/provider.dart';
import 'package:nethive_neo/providers/nethive/componentes_provider.dart';
class NegociosTable extends StatelessWidget {
final EmpresasNegociosProvider provider;
const NegociosTable({
Key? key,
required this.provider,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return PlutoGrid(
key: UniqueKey(),
configuration: PlutoGridConfiguration(
enableMoveDownAfterSelecting: true,
enableMoveHorizontalInEditing: true,
localeText: const PlutoGridLocaleText.spanish(),
scrollbar: PlutoGridScrollbarConfig(
draggableScrollbar: true,
isAlwaysShown: false,
onlyDraggingThumb: true,
enableScrollAfterDragEnd: true,
scrollbarThickness: 12,
scrollbarThicknessWhileDragging: 16,
hoverWidth: 20,
scrollBarColor: AppTheme.of(context).primaryColor.withOpacity(0.7),
scrollBarTrackColor: Colors.grey.withOpacity(0.2),
scrollbarRadius: const Radius.circular(8),
scrollbarRadiusWhileDragging: const Radius.circular(10),
),
style: PlutoGridStyleConfig(
gridBorderColor: Colors.grey.withOpacity(0.3),
activatedBorderColor: AppTheme.of(context).primaryColor,
inactivatedBorderColor: Colors.grey.withOpacity(0.3),
gridBackgroundColor: AppTheme.of(context).primaryBackground,
rowColor: AppTheme.of(context).secondaryBackground,
activatedColor: AppTheme.of(context).primaryColor.withOpacity(0.1),
checkedColor: AppTheme.of(context).primaryColor.withOpacity(0.2),
cellTextStyle: TextStyle(
color: AppTheme.of(context).primaryText,
fontSize: 14,
),
columnTextStyle: TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.bold,
),
menuBackgroundColor: AppTheme.of(context).secondaryBackground,
gridBorderRadius: BorderRadius.circular(8),
rowHeight: 70,
),
columnFilter: const PlutoGridColumnFilterConfig(
filters: [
...FilterHelper.defaultFilters,
],
),
),
columns: [
PlutoColumn(
title: 'ID',
field: 'id',
titleTextAlign: PlutoColumnTextAlign.center,
textAlign: PlutoColumnTextAlign.center,
width: 100,
type: PlutoColumnType.text(),
enableEditingMode: false,
backgroundColor: AppTheme.of(context).primaryColor,
enableContextMenu: false,
enableDropToResize: false,
renderer: (rendererContext) {
return Text(
rendererContext.cell.value.toString().substring(0, 8) + '...',
textAlign: TextAlign.center,
style: TextStyle(
color: AppTheme.of(context).primaryText,
fontSize: 12,
fontWeight: FontWeight.w500,
),
);
},
),
PlutoColumn(
title: 'Nombre de Sucursal',
field: 'nombre',
titleTextAlign: PlutoColumnTextAlign.center,
textAlign: PlutoColumnTextAlign.center,
width: 200,
type: PlutoColumnType.text(),
enableEditingMode: false,
backgroundColor: AppTheme.of(context).primaryColor,
enableContextMenu: false,
enableDropToResize: false,
renderer: (rendererContext) {
return Container(
padding: const EdgeInsets.all(8),
child: Row(
children: [
// Logo del negocio
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: AppTheme.of(context).primaryColor,
borderRadius: BorderRadius.circular(6),
),
child:
rendererContext.row.cells['logo_url']?.value != null &&
rendererContext.row.cells['logo_url']!.value
.toString()
.isNotEmpty
? ClipRRect(
borderRadius: BorderRadius.circular(6),
child: Image.network(
rendererContext.row.cells['logo_url']!.value
.toString(),
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Image.asset(
'assets/images/placeholder_no_image.jpg',
fit: BoxFit.cover,
);
},
),
)
: Image.asset(
'assets/images/placeholder_no_image.jpg',
fit: BoxFit.cover,
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
rendererContext.cell.value.toString(),
style: TextStyle(
color: AppTheme.of(context).primaryText,
fontSize: 14,
fontWeight: FontWeight.w600,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
);
},
),
PlutoColumn(
title: 'Ciudad',
field: 'direccion',
titleTextAlign: PlutoColumnTextAlign.center,
textAlign: PlutoColumnTextAlign.center,
width: 180,
type: PlutoColumnType.text(),
enableEditingMode: false,
backgroundColor: AppTheme.of(context).primaryColor,
enableContextMenu: false,
enableDropToResize: false,
renderer: (rendererContext) {
// Extraer solo la ciudad de la dirección completa
String direccionCompleta = rendererContext.cell.value.toString();
String ciudad = _extraerCiudad(direccionCompleta);
return Container(
padding: const EdgeInsets.all(8),
child: Text(
ciudad,
textAlign: TextAlign.center,
style: TextStyle(
color: AppTheme.of(context).primaryText,
fontSize: 14,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
);
},
),
PlutoColumn(
title: 'Empleados',
field: 'tipo_local',
titleTextAlign: PlutoColumnTextAlign.center,
textAlign: PlutoColumnTextAlign.center,
width: 120,
type: PlutoColumnType.text(),
enableEditingMode: false,
backgroundColor: AppTheme.of(context).primaryColor,
enableContextMenu: false,
enableDropToResize: false,
renderer: (rendererContext) {
// Simulamos cantidad de empleados basado en el tipo de local
String empleados =
rendererContext.cell.value.toString() == 'Sucursal'
? '95'
: '120';
return Container(
padding: const EdgeInsets.all(8),
child: Text(
'$empleados empleados',
textAlign: TextAlign.center,
style: TextStyle(
color: AppTheme.of(context).primaryText,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
);
},
),
PlutoColumn(
title: 'Dirección Completa',
field: 'direccion_completa',
titleTextAlign: PlutoColumnTextAlign.center,
textAlign: PlutoColumnTextAlign.center,
width: 280,
type: PlutoColumnType.text(),
enableEditingMode: false,
backgroundColor: AppTheme.of(context).primaryColor,
enableContextMenu: false,
enableDropToResize: false,
renderer: (rendererContext) {
// Usamos la dirección del row en lugar del cell
String direccion =
rendererContext.row.cells['direccion']?.value.toString() ?? '';
return Container(
padding: const EdgeInsets.all(8),
child: Text(
direccion,
style: TextStyle(
color: AppTheme.of(context).primaryText,
fontSize: 14,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
);
},
),
PlutoColumn(
title: 'Coordenadas',
field: 'latitud',
titleTextAlign: PlutoColumnTextAlign.center,
textAlign: PlutoColumnTextAlign.center,
width: 150,
type: PlutoColumnType.text(),
enableEditingMode: false,
backgroundColor: AppTheme.of(context).primaryColor,
enableContextMenu: false,
enableDropToResize: false,
renderer: (rendererContext) {
final latitud = rendererContext.row.cells['latitud']?.value ?? '0';
final longitud =
rendererContext.row.cells['longitud']?.value ?? '0';
return Container(
padding: const EdgeInsets.all(8),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Lat: ${double.parse(latitud.toString()).toStringAsFixed(4)}',
style: TextStyle(
color: AppTheme.of(context).primaryText,
fontSize: 12,
),
),
Text(
'Lng: ${double.parse(longitud.toString()).toStringAsFixed(4)}',
style: TextStyle(
color: AppTheme.of(context).primaryText,
fontSize: 12,
),
),
],
),
);
},
),
PlutoColumn(
title: 'Infraestructura',
field: 'acceder_infraestructura',
titleTextAlign: PlutoColumnTextAlign.center,
textAlign: PlutoColumnTextAlign.center,
width: 200,
type: PlutoColumnType.text(),
enableEditingMode: false,
backgroundColor: AppTheme.of(context).primaryColor,
enableContextMenu: false,
enableDropToResize: false,
renderer: (rendererContext) {
return Container(
padding: const EdgeInsets.all(8),
child: Center(
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.orange.shade600,
Colors.deepOrange.shade500,
],
),
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.orange.withOpacity(0.4),
blurRadius: 8,
offset: const Offset(0, 3),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
final negocioId =
rendererContext.row.cells['id']?.value;
final negocioNombre =
rendererContext.row.cells['nombre']?.value;
final empresaId =
rendererContext.row.cells['empresa_id']?.value;
if (negocioId != null &&
negocioNombre != null &&
empresaId != null) {
// Establecer el contexto del negocio en ComponentesProvider
final componentesProvider =
Provider.of<ComponentesProvider>(context,
listen: false);
componentesProvider
.setNegocioSeleccionado(
negocioId,
negocioNombre,
empresaId,
)
.then((_) {
// Navegar al layout principal con el negocio seleccionado
if (context.mounted) {
context.go('/infrastructure/$negocioId');
}
});
}
},
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.developer_board,
color: Colors.white,
size: 18,
),
const SizedBox(width: 8),
Text(
'Acceder a\nInfraestructura',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
height: 1.2,
),
),
],
),
),
),
),
),
),
);
},
),
PlutoColumn(
title: 'Acciones',
field: 'editar',
titleTextAlign: PlutoColumnTextAlign.center,
textAlign: PlutoColumnTextAlign.center,
width: 120,
type: PlutoColumnType.text(),
enableEditingMode: false,
backgroundColor: AppTheme.of(context).primaryColor,
enableContextMenu: false,
enableDropToResize: false,
renderer: (rendererContext) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Botón editar
Tooltip(
message: 'Editar negocio',
child: InkWell(
onTap: () {
// TODO: Implementar edición
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Función de edición próximamente')),
);
},
child: Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: AppTheme.of(context).primaryColor,
borderRadius: BorderRadius.circular(4),
),
child: const Icon(
Icons.edit,
color: Colors.white,
size: 16,
),
),
),
),
const SizedBox(width: 8),
// Botón ver componentes
Tooltip(
message: 'Ver componentes',
child: InkWell(
onTap: () {
// TODO: Navegar a componentes
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Navegando a componentes de ${rendererContext.row.cells['nombre']?.value}',
),
),
);
},
child: Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: Colors.orange,
borderRadius: BorderRadius.circular(4),
),
child: const Icon(
Icons.inventory_2,
color: Colors.white,
size: 16,
),
),
),
),
const SizedBox(width: 8),
// Botón eliminar
Tooltip(
message: 'Eliminar negocio',
child: InkWell(
onTap: () {
_showDeleteDialog(
context,
rendererContext.row.cells['id']?.value,
rendererContext.row.cells['nombre']?.value,
);
},
child: Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(4),
),
child: const Icon(
Icons.delete,
color: Colors.white,
size: 16,
),
),
),
),
],
);
},
),
],
rows: provider.negociosRows,
onLoaded: (event) {
provider.negociosStateManager = event.stateManager;
},
createFooter: (stateManager) {
stateManager.setPageSize(10, notify: false);
return PlutoPagination(stateManager);
},
);
}
void _showDeleteDialog(
BuildContext context, String? negocioId, String? nombre) {
if (negocioId == null) return;
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
backgroundColor: AppTheme.of(context).primaryBackground,
title: const Text('Confirmar eliminación'),
content: Text(
'¿Estás seguro de que deseas eliminar la sucursal "$nombre"?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(
'Cancelar',
style: TextStyle(color: AppTheme.of(context).secondaryText),
),
),
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: 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',
style: TextStyle(color: Colors.red),
),
),
],
);
},
);
}
String _extraerCiudad(String direccionCompleta) {
// Lógica para extraer la ciudad de la dirección completa
// Suponiendo que la ciudad es la segunda palabra en la dirección
List<String> partes = direccionCompleta.split(',');
return partes.length > 1 ? partes[1].trim() : direccionCompleta;
}
}

View File

@@ -1,582 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:nethive_neo/providers/nethive/navigation_provider.dart';
import 'package:nethive_neo/providers/nethive/componentes_provider.dart';
import 'package:nethive_neo/pages/infrastructure/widgets/infrastructure_sidemenu.dart';
import 'package:nethive_neo/pages/infrastructure/widgets/mobile_navigation_modal.dart';
import 'package:nethive_neo/pages/infrastructure/pages/dashboard_page.dart';
import 'package:nethive_neo/pages/infrastructure/pages/inventario_page.dart';
import 'package:nethive_neo/pages/infrastructure/pages/topologia_page.dart';
import 'package:nethive_neo/pages/infrastructure/pages/alertas_page.dart';
import 'package:nethive_neo/pages/infrastructure/pages/configuracion_page.dart';
import 'package:nethive_neo/theme/theme.dart';
class InfrastructureLayout extends StatefulWidget {
final String negocioId;
const InfrastructureLayout({
Key? key,
required this.negocioId,
}) : super(key: key);
@override
State<InfrastructureLayout> createState() => _InfrastructureLayoutState();
}
class _InfrastructureLayoutState extends State<InfrastructureLayout>
with TickerProviderStateMixin {
bool _isSidebarExpanded = true;
late AnimationController _fadeController;
late Animation<double> _fadeAnimation;
@override
void initState() {
super.initState();
_fadeController = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _fadeController,
curve: Curves.easeInOut,
));
// Establecer el negocio seleccionado
WidgetsBinding.instance.addPostFrameCallback((_) async {
// Primero establecer en NavigationProvider
context
.read<NavigationProvider>()
.setNegocioSeleccionado(widget.negocioId);
// Luego obtener la información completa y establecer en ComponentesProvider
await _setupComponentesProvider();
_fadeController.forward();
});
}
Future<void> _setupComponentesProvider() async {
try {
final navigationProvider = context.read<NavigationProvider>();
final componentesProvider = context.read<ComponentesProvider>();
// Esperar a que NavigationProvider cargue la información del negocio
await Future.delayed(const Duration(milliseconds: 100));
final negocio = navigationProvider.negocioSeleccionado;
final empresa = navigationProvider.empresaSeleccionada;
if (negocio != null && empresa != null) {
// Establecer el contexto completo en ComponentesProvider
await componentesProvider.setNegocioSeleccionado(
negocio.id,
negocio.nombre,
empresa.id,
);
}
} catch (e) {
print('Error al configurar ComponentesProvider: ${e.toString()}');
}
}
@override
void dispose() {
_fadeController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final isLargeScreen = MediaQuery.of(context).size.width > 1200;
final isMediumScreen = MediaQuery.of(context).size.width > 800;
// Ajustar sidebar basado en tamaño de pantalla
if (!isLargeScreen && _isSidebarExpanded) {
_isSidebarExpanded = false;
}
return Scaffold(
backgroundColor: AppTheme.of(context).primaryBackground,
body: FadeTransition(
opacity: _fadeAnimation,
child: Container(
decoration: BoxDecoration(
gradient: AppTheme.of(context).darkBackgroundGradient,
),
child: Consumer<NavigationProvider>(
builder: (context, navigationProvider, child) {
if (navigationProvider.negocioSeleccionado == null) {
return _buildLoadingScreen();
}
if (isMediumScreen) {
// Vista desktop/tablet
return Row(
children: [
// Sidebar
InfrastructureSidemenu(
isExpanded: _isSidebarExpanded,
onToggle: () {
setState(() {
_isSidebarExpanded = !_isSidebarExpanded;
});
},
),
// Área principal
Expanded(
child: Column(
children: [
// Header superior
_buildHeader(navigationProvider),
// Contenido principal
Expanded(
child: _buildMainContent(navigationProvider),
),
],
),
),
],
);
} else {
// Vista móvil
return Column(
children: [
// Header móvil
_buildMobileHeader(navigationProvider),
// Contenido principal
Expanded(
child: _buildMainContent(navigationProvider),
),
],
);
}
},
),
),
),
// Drawer para móvil
drawer: MediaQuery.of(context).size.width <= 800
? Drawer(
backgroundColor: Colors.transparent,
child: InfrastructureSidemenu(
isExpanded: true,
onToggle: () => Navigator.of(context).pop(),
),
)
: null,
);
}
Widget _buildLoadingScreen() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: AppTheme.of(context).primaryGradient,
shape: BoxShape.circle,
),
child: const CircularProgressIndicator(
color: Colors.white,
strokeWidth: 3,
),
),
const SizedBox(height: 20),
Text(
'Cargando infraestructura...',
style: TextStyle(
color: AppTheme.of(context).primaryText,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
],
),
);
}
Widget _buildHeader(NavigationProvider navigationProvider) {
final negocio = navigationProvider.negocioSeleccionado!;
final empresa = navigationProvider.empresaSeleccionada!;
final currentMenuItem = navigationProvider.getMenuItemByIndex(
navigationProvider.selectedMenuIndex,
);
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppTheme.of(context).secondaryBackground,
border: Border(
bottom: BorderSide(
color: AppTheme.of(context).primaryColor.withOpacity(0.2),
width: 1,
),
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
// Logo solo de Nethive
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
gradient: AppTheme.of(context).primaryGradient,
borderRadius: BorderRadius.circular(12),
),
child: Image.asset(
'assets/images/favicon.png',
width: 48,
height: 48,
),
),
const SizedBox(width: 20),
// Breadcrumb mejorado
Expanded(
child: Row(
children: [
// Empresa
Text(
empresa.nombre,
style: TextStyle(
color: AppTheme.of(context).secondaryText,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(width: 8),
Icon(
Icons.arrow_forward_ios,
size: 12,
color: AppTheme.of(context).secondaryText,
),
const SizedBox(width: 8),
// Negocio (cuadro verde como en la imagen de referencia)
Container(
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
negocio.nombre,
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
Text(
'(${empresa.nombre})',
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 10,
),
),
],
),
),
const SizedBox(width: 8),
Icon(
Icons.arrow_forward_ios,
size: 12,
color: AppTheme.of(context).secondaryText,
),
const SizedBox(width: 8),
// Página actual
Text(
currentMenuItem.title,
style: TextStyle(
color: AppTheme.of(context).primaryColor,
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
],
),
),
// Buscador (conservado como en la referencia)
Container(
width: 300,
height: 40,
decoration: BoxDecoration(
color: AppTheme.of(context).formBackground,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: AppTheme.of(context).primaryColor.withOpacity(0.3),
),
),
child: TextField(
style: TextStyle(color: AppTheme.of(context).primaryText),
decoration: InputDecoration(
hintText: 'Buscar en infraestructura...',
hintStyle: TextStyle(
color: AppTheme.of(context).hintText,
fontSize: 14,
),
prefixIcon: Icon(
Icons.search,
color: AppTheme.of(context).primaryColor,
size: 20,
),
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 10,
),
),
),
),
],
),
);
}
Widget _buildMobileHeader(NavigationProvider navigationProvider) {
final negocio = navigationProvider.negocioSeleccionado!;
final currentMenuItem = navigationProvider.getMenuItemByIndex(
navigationProvider.selectedMenuIndex,
);
return Container(
padding: const EdgeInsets.fromLTRB(16, 40, 16, 16),
decoration: BoxDecoration(
gradient: AppTheme.of(context).primaryGradient,
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(20),
bottomRight: Radius.circular(20),
),
boxShadow: [
BoxShadow(
color: AppTheme.of(context).primaryColor.withOpacity(0.3),
blurRadius: 15,
offset: const Offset(0, 5),
),
],
),
child: Column(
children: [
Row(
children: [
// Botón de menú moderno que abre el modal
GestureDetector(
onTap: () => _showMobileNavigationModal(),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.white.withOpacity(0.3),
width: 1,
),
),
child: const Icon(
Icons.menu,
color: Colors.white,
size: 24,
),
),
),
const SizedBox(width: 16),
// Logo
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
),
child: Image.asset(
'assets/images/favicon.png',
width: 24,
height: 24,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'NETHIVE',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
letterSpacing: 1.2,
),
),
Text(
currentMenuItem.title,
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
],
),
),
// Indicador de módulo actual
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
currentMenuItem.icon,
color: Colors.white,
size: 20,
),
),
],
),
const SizedBox(height: 16),
// Info del negocio mejorada
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.15),
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: Colors.white.withOpacity(0.3),
width: 1,
),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.location_on,
color: Colors.white,
size: 16,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
negocio.nombre,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
Text(
negocio.tipoLocal,
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
],
),
),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.8),
borderRadius: BorderRadius.circular(10),
),
child: const Text(
'Activo',
style: TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
],
),
);
}
// Método para mostrar el modal de navegación móvil
void _showMobileNavigationModal() {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
barrierColor: Colors.black.withOpacity(0.5),
builder: (context) => DraggableScrollableSheet(
initialChildSize: 0.85,
maxChildSize: 0.95,
minChildSize: 0.3,
builder: (context, scrollController) => Container(
decoration: BoxDecoration(
color: AppTheme.of(context).primaryBackground,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(25),
topRight: Radius.circular(25),
),
),
child: const MobileNavigationModal(),
),
),
);
}
Widget _buildMainContent(NavigationProvider navigationProvider) {
switch (navigationProvider.selectedMenuIndex) {
case 0:
return const DashboardPage();
case 1:
return const InventarioPage();
case 2:
return const TopologiaPage();
case 3:
return const AlertasPage();
case 4:
return const ConfiguracionPage();
default:
return const DashboardPage();
}
}
}

View File

@@ -1,110 +0,0 @@
import 'package:flutter/material.dart';
import 'package:nethive_neo/theme/theme.dart';
class AlertasPage extends StatelessWidget {
const AlertasPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: AppTheme.of(context).primaryGradient,
borderRadius: BorderRadius.circular(16),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.warning,
color: Colors.white,
size: 24,
),
),
const SizedBox(width: 16),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Centro de Alertas',
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
Text(
'Monitoreo y gestión de alertas MDF/IDF',
style: TextStyle(
color: Colors.white70,
fontSize: 14,
),
),
],
),
),
],
),
),
const SizedBox(height: 24),
// Contenido próximamente
Expanded(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.orange, Colors.red],
),
shape: BoxShape.circle,
),
child: const Icon(
Icons.warning,
size: 60,
color: Colors.white,
),
),
const SizedBox(height: 20),
Text(
'Centro de Alertas',
style: TextStyle(
color: AppTheme.of(context).primaryText,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
Text(
'Sistema de monitoreo y alertas para infraestructura\nPróximamente disponible',
textAlign: TextAlign.center,
style: TextStyle(
color: AppTheme.of(context).secondaryText,
fontSize: 16,
),
),
],
),
),
),
],
),
);
}
}

View File

@@ -1,110 +0,0 @@
import 'package:flutter/material.dart';
import 'package:nethive_neo/theme/theme.dart';
class ConfiguracionPage extends StatelessWidget {
const ConfiguracionPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: AppTheme.of(context).primaryGradient,
borderRadius: BorderRadius.circular(16),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.settings,
color: Colors.white,
size: 24,
),
),
const SizedBox(width: 16),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Configuración',
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
Text(
'Configuración de sistema y infraestructura',
style: TextStyle(
color: Colors.white70,
fontSize: 14,
),
),
],
),
),
],
),
),
const SizedBox(height: 24),
// Contenido próximamente
Expanded(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.purple, Colors.blue],
),
shape: BoxShape.circle,
),
child: const Icon(
Icons.settings,
size: 60,
color: Colors.white,
),
),
const SizedBox(height: 20),
Text(
'Configuración del Sistema',
style: TextStyle(
color: AppTheme.of(context).primaryText,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
Text(
'Panel de configuración para infraestructura MDF/IDF\nPróximamente disponible',
textAlign: TextAlign.center,
style: TextStyle(
color: AppTheme.of(context).secondaryText,
fontSize: 16,
),
),
],
),
),
),
],
),
);
}
}

View File

@@ -1,828 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:nethive_neo/providers/nethive/navigation_provider.dart';
import 'package:nethive_neo/providers/nethive/componentes_provider.dart';
import 'package:nethive_neo/theme/theme.dart';
class DashboardPage extends StatefulWidget {
const DashboardPage({Key? key}) : super(key: key);
@override
State<DashboardPage> createState() => _DashboardPageState();
}
class _DashboardPageState extends State<DashboardPage>
with TickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
_animationController.forward();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final isLargeScreen = MediaQuery.of(context).size.width > 1200;
final isMediumScreen = MediaQuery.of(context).size.width > 800;
final isSmallScreen = MediaQuery.of(context).size.width <= 600;
return FadeTransition(
opacity: _fadeAnimation,
child: Consumer2<NavigationProvider, ComponentesProvider>(
builder: (context, navigationProvider, componentesProvider, child) {
return Container(
padding: EdgeInsets.all(isSmallScreen ? 12 : 24),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Título de la página
_buildPageTitle(isSmallScreen),
SizedBox(height: isSmallScreen ? 16 : 24),
// Cards de estadísticas principales
_buildStatsCards(componentesProvider, isLargeScreen,
isMediumScreen, isSmallScreen),
SizedBox(height: isSmallScreen ? 16 : 24),
// Gráficos y métricas
_buildContentSection(componentesProvider, isLargeScreen,
isMediumScreen, isSmallScreen),
SizedBox(height: isSmallScreen ? 16 : 24),
// Actividad reciente
_buildActivityFeed(
isLargeScreen, isMediumScreen, isSmallScreen),
],
),
),
);
},
),
);
}
Widget _buildPageTitle(bool isSmallScreen) {
return Container(
padding: EdgeInsets.all(isSmallScreen ? 16 : 20),
decoration: BoxDecoration(
gradient: AppTheme.of(context).primaryGradient,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: AppTheme.of(context).primaryColor.withOpacity(0.3),
blurRadius: 15,
offset: const Offset(0, 5),
),
],
),
child: Row(
children: [
Container(
padding: EdgeInsets.all(isSmallScreen ? 8 : 12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
Icons.dashboard,
color: Colors.white,
size: isSmallScreen ? 20 : 24,
),
),
SizedBox(width: isSmallScreen ? 12 : 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Dashboard MDF/IDF',
style: TextStyle(
color: Colors.white,
fontSize: isSmallScreen ? 18 : 24,
fontWeight: FontWeight.bold,
),
),
if (!isSmallScreen) ...[
Text(
'Panel de control de infraestructura de red',
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 14,
),
),
],
],
),
),
],
),
);
}
Widget _buildStatsCards(ComponentesProvider componentesProvider,
bool isLargeScreen, bool isMediumScreen, bool isSmallScreen) {
final stats = [
{
'title': 'Componentes Totales',
'value': '${componentesProvider.componentes.length}',
'icon': Icons.inventory_2,
'color': Colors.blue,
'subtitle': 'equipos registrados',
},
{
'title': 'Componentes Activos',
'value':
'${componentesProvider.componentes.where((c) => c.activo).length}',
'icon': Icons.power,
'color': Colors.green,
'subtitle': 'en funcionamiento',
},
{
'title': 'En Uso',
'value':
'${componentesProvider.componentes.where((c) => c.enUso).length}',
'icon': Icons.trending_up,
'color': Colors.orange,
'subtitle': 'siendo utilizados',
},
{
'title': 'Categorías',
'value': '${componentesProvider.categorias.length}',
'icon': Icons.category,
'color': Colors.purple,
'subtitle': 'tipos de equipos',
},
];
if (isSmallScreen) {
// En móvil: 2x2 grid
return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 1.1,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
itemCount: stats.length,
itemBuilder: (context, index) {
final stat = stats[index];
return _buildStatCard(
stat['title'] as String,
stat['value'] as String,
stat['icon'] as IconData,
stat['color'] as Color,
stat['subtitle'] as String,
isSmallScreen,
);
},
);
} else {
// En desktop/tablet: row horizontal
return Row(
children: stats.map((stat) {
final isLast = stat == stats.last;
return Expanded(
child: Row(
children: [
Expanded(
child: _buildStatCard(
stat['title'] as String,
stat['value'] as String,
stat['icon'] as IconData,
stat['color'] as Color,
stat['subtitle'] as String,
isSmallScreen,
),
),
if (!isLast) const SizedBox(width: 16),
],
),
);
}).toList(),
);
}
}
Widget _buildStatCard(
String title,
String value,
IconData icon,
Color color,
String subtitle,
bool isSmallScreen,
) {
return TweenAnimationBuilder<double>(
duration: const Duration(milliseconds: 800),
tween: Tween(begin: 0.0, end: 1.0),
builder: (context, animationValue, child) {
return Transform.scale(
scale: 0.8 + (0.2 * animationValue),
child: Container(
padding: EdgeInsets.all(isSmallScreen ? 12 : 20),
decoration: BoxDecoration(
color: AppTheme.of(context).secondaryBackground,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: color.withOpacity(0.3),
width: 1,
),
boxShadow: [
BoxShadow(
color: color.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Container(
padding: EdgeInsets.all(isSmallScreen ? 6 : 8),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon,
color: color, size: isSmallScreen ? 16 : 20),
),
const Spacer(),
if (!isSmallScreen) ...[
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'MDF/IDF',
style: TextStyle(
color: color,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
],
],
),
SizedBox(height: isSmallScreen ? 8 : 16),
TweenAnimationBuilder<int>(
duration: Duration(
milliseconds: 1000 + (animationValue * 500).round()),
tween: IntTween(begin: 0, end: int.tryParse(value) ?? 0),
builder: (context, animatedValue, child) {
return Text(
animatedValue.toString(),
style: TextStyle(
color: AppTheme.of(context).primaryText,
fontSize: isSmallScreen ? 20 : 28,
fontWeight: FontWeight.bold,
),
);
},
),
const SizedBox(height: 4),
Text(
title,
style: TextStyle(
color: AppTheme.of(context).primaryText,
fontSize: isSmallScreen ? 12 : 14,
fontWeight: FontWeight.w600,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
if (!isSmallScreen) ...[
Text(
subtitle,
style: TextStyle(
color: AppTheme.of(context).secondaryText,
fontSize: 12,
),
),
],
],
),
),
);
},
);
}
Widget _buildContentSection(ComponentesProvider componentesProvider,
bool isLargeScreen, bool isMediumScreen, bool isSmallScreen) {
if (isSmallScreen) {
// En móvil: columna vertical
return Column(
children: [
_buildComponentsOverview(componentesProvider, isSmallScreen),
const SizedBox(height: 16),
_buildAlertasRecientes(isSmallScreen),
],
);
} else {
// En desktop/tablet: row horizontal
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
flex: 2,
child: _buildComponentsOverview(componentesProvider, isSmallScreen),
),
const SizedBox(width: 24),
Expanded(
child: _buildAlertasRecientes(isSmallScreen),
),
],
);
}
}
Widget _buildComponentsOverview(
ComponentesProvider componentesProvider, bool isSmallScreen) {
return Container(
padding: EdgeInsets.all(isSmallScreen ? 16 : 20),
decoration: BoxDecoration(
color: AppTheme.of(context).secondaryBackground,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: AppTheme.of(context).primaryColor.withOpacity(0.2),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.pie_chart,
color: AppTheme.of(context).primaryColor,
size: isSmallScreen ? 18 : 20,
),
SizedBox(width: isSmallScreen ? 6 : 8),
Expanded(
child: Text(
isSmallScreen
? 'Componentes por Categoría'
: 'Distribución de Componentes por Categoría',
style: TextStyle(
color: AppTheme.of(context).primaryText,
fontSize: isSmallScreen ? 14 : 16,
fontWeight: FontWeight.bold,
),
),
),
],
),
SizedBox(height: isSmallScreen ? 16 : 20),
...componentesProvider.categorias
.take(isSmallScreen ? 4 : 5)
.map((categoria) {
final componentesDeCategoria = componentesProvider.componentes
.where((c) => c.categoriaId == categoria.id)
.length;
final porcentaje = componentesProvider.componentes.isNotEmpty
? (componentesDeCategoria /
componentesProvider.componentes.length *
100)
: 0.0;
return Container(
margin: EdgeInsets.only(bottom: isSmallScreen ? 8 : 12),
child: Column(
children: [
Row(
children: [
Expanded(
flex: isSmallScreen ? 2 : 3,
child: Text(
categoria.nombre,
style: TextStyle(
color: AppTheme.of(context).primaryText,
fontSize: isSmallScreen ? 12 : 14,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
SizedBox(width: isSmallScreen ? 6 : 8),
Expanded(
flex: isSmallScreen ? 3 : 4,
child: Container(
height: isSmallScreen ? 6 : 8,
decoration: BoxDecoration(
color: AppTheme.of(context).tertiaryBackground,
borderRadius: BorderRadius.circular(4),
),
child: FractionallySizedBox(
alignment: Alignment.centerLeft,
widthFactor: porcentaje / 100,
child: Container(
decoration: BoxDecoration(
gradient: AppTheme.of(context).primaryGradient,
borderRadius: BorderRadius.circular(4),
),
),
),
),
),
SizedBox(width: isSmallScreen ? 6 : 8),
SizedBox(
width: isSmallScreen ? 40 : 60,
child: Text(
isSmallScreen
? '$componentesDeCategoria'
: '$componentesDeCategoria (${porcentaje.toStringAsFixed(1)}%)',
style: TextStyle(
color: AppTheme.of(context).secondaryText,
fontSize: isSmallScreen ? 10 : 12,
),
textAlign: TextAlign.end,
),
),
],
),
],
),
);
}).toList(),
],
),
);
}
Widget _buildAlertasRecientes(bool isSmallScreen) {
final alertas = [
{
'tipo': 'Warning',
'mensaje': 'Switch en Rack 3 sobrecalentándose',
'tiempo': '5 min'
},
{
'tipo': 'Error',
'mensaje': 'Pérdida de conectividad en Panel A4',
'tiempo': '12 min'
},
{
'tipo': 'Info',
'mensaje': 'Mantenimiento programado completado',
'tiempo': '1 hr'
},
if (!isSmallScreen)
{
'tipo': 'Warning',
'mensaje': 'Capacidad de cable al 85%',
'tiempo': '2 hrs'
},
];
return Container(
padding: EdgeInsets.all(isSmallScreen ? 16 : 20),
decoration: BoxDecoration(
color: AppTheme.of(context).secondaryBackground,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: AppTheme.of(context).primaryColor.withOpacity(0.2),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.warning,
color: Colors.orange,
size: isSmallScreen ? 18 : 20,
),
SizedBox(width: isSmallScreen ? 6 : 8),
Text(
'Alertas Recientes',
style: TextStyle(
color: AppTheme.of(context).primaryText,
fontSize: isSmallScreen ? 14 : 16,
fontWeight: FontWeight.bold,
),
),
],
),
SizedBox(height: isSmallScreen ? 12 : 16),
...alertas.map((alerta) {
Color alertColor;
IconData alertIcon;
switch (alerta['tipo']) {
case 'Error':
alertColor = Colors.red;
alertIcon = Icons.error;
break;
case 'Warning':
alertColor = Colors.orange;
alertIcon = Icons.warning;
break;
default:
alertColor = Colors.blue;
alertIcon = Icons.info;
}
return Container(
margin: EdgeInsets.only(bottom: isSmallScreen ? 8 : 12),
padding: EdgeInsets.all(isSmallScreen ? 8 : 12),
decoration: BoxDecoration(
color: alertColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: alertColor.withOpacity(0.3),
),
),
child: Row(
children: [
Icon(alertIcon,
color: alertColor, size: isSmallScreen ? 14 : 16),
SizedBox(width: isSmallScreen ? 6 : 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
alerta['mensaje']!,
style: TextStyle(
color: AppTheme.of(context).primaryText,
fontSize: isSmallScreen ? 11 : 12,
fontWeight: FontWeight.w500,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
Text(
'hace ${alerta['tiempo']}',
style: TextStyle(
color: AppTheme.of(context).secondaryText,
fontSize: isSmallScreen ? 9 : 10,
),
),
],
),
),
],
),
);
}).toList(),
],
),
);
}
Widget _buildActivityFeed(
bool isLargeScreen, bool isMediumScreen, bool isSmallScreen) {
return Container(
padding: EdgeInsets.all(isSmallScreen ? 16 : 20),
decoration: BoxDecoration(
color: AppTheme.of(context).secondaryBackground,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: AppTheme.of(context).primaryColor.withOpacity(0.2),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.timeline,
color: AppTheme.of(context).primaryColor,
size: isSmallScreen ? 18 : 20,
),
SizedBox(width: isSmallScreen ? 6 : 8),
Text(
'Actividad Reciente',
style: TextStyle(
color: AppTheme.of(context).primaryText,
fontSize: isSmallScreen ? 14 : 16,
fontWeight: FontWeight.bold,
),
),
],
),
SizedBox(height: isSmallScreen ? 12 : 16),
_buildActivityItems(isSmallScreen),
],
),
);
}
Widget _buildActivityItems(bool isSmallScreen) {
final activities = [
{
'title': 'Nuevo componente añadido',
'description': 'Switch Cisco SG300-28 registrado en Rack 5',
'time': '10:30 AM',
'icon': Icons.add_circle,
'color': Colors.green,
},
{
'title': 'Mantenimiento completado',
'description': 'Revisión de cables en Panel Principal',
'time': '09:15 AM',
'icon': Icons.build_circle,
'color': Colors.blue,
},
{
'title': 'Configuración actualizada',
'description': 'Parámetros de red modificados',
'time': '08:45 AM',
'icon': Icons.settings,
'color': Colors.purple,
},
];
if (isSmallScreen) {
// En móvil: lista vertical
return Column(
children: activities.map((activity) {
return Container(
margin: const EdgeInsets.only(bottom: 12),
child: _buildActivityItem(
activity['title'] as String,
activity['description'] as String,
activity['time'] as String,
activity['icon'] as IconData,
activity['color'] as Color,
isSmallScreen,
),
);
}).toList(),
);
} else {
// En desktop/tablet: fila horizontal
return Row(
children: activities.map((activity) {
final isLast = activity == activities.last;
return Expanded(
child: Row(
children: [
Expanded(
child: _buildActivityItem(
activity['title'] as String,
activity['description'] as String,
activity['time'] as String,
activity['icon'] as IconData,
activity['color'] as Color,
isSmallScreen,
),
),
if (!isLast) const SizedBox(width: 16),
],
),
);
}).toList(),
);
}
}
Widget _buildActivityItem(
String title,
String description,
String time,
IconData icon,
Color color,
bool isSmallScreen,
) {
return Container(
padding: EdgeInsets.all(isSmallScreen ? 12 : 16),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: color.withOpacity(0.3),
),
),
child: isSmallScreen
? 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: 16),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
title,
style: TextStyle(
color: AppTheme.of(context).primaryText,
fontSize: 12,
fontWeight: FontWeight.w600,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
Text(
time,
style: TextStyle(
color: color,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 4),
Text(
description,
style: TextStyle(
color: AppTheme.of(context).secondaryText,
fontSize: 11,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
)
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, color: color, size: 16),
const SizedBox(width: 8),
Text(
time,
style: TextStyle(
color: color,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 8),
Text(
title,
style: TextStyle(
color: AppTheme.of(context).primaryText,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
description,
style: TextStyle(
color: AppTheme.of(context).secondaryText,
fontSize: 12,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
);
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,213 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:nethive_neo/providers/nethive/componentes_provider.dart';
class FloorPlanViewWidget extends StatelessWidget {
final bool isMediumScreen;
final ComponentesProvider provider;
const FloorPlanViewWidget({
Key? key,
required this.isMediumScreen,
required this.provider,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(24),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.map,
size: 80,
color: Colors.white.withOpacity(0.7),
).animate().scale(duration: 600.ms),
const SizedBox(height: 20),
const Text(
'Plano de Planta',
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
).animate().fadeIn(delay: 300.ms),
const SizedBox(height: 8),
Text(
'Próximamente: Distribución geográfica de componentes\ncon ${_getUbicacionesUnicas().length} ubicaciones identificadas',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white.withOpacity(0.7),
fontSize: 16,
),
).animate().fadeIn(delay: 500.ms),
const SizedBox(height: 24),
// Panel de información adicional para planos
if (isMediumScreen) _buildFloorPlanInfoPanel(),
],
),
),
);
}
List<String> _getUbicacionesUnicas() {
final ubicaciones = provider.componentesTopologia
.where((c) => c.ubicacion != null && c.ubicacion!.trim().isNotEmpty)
.map((c) => c.ubicacion!)
.toSet()
.toList();
return ubicaciones;
}
Widget _buildFloorPlanInfoPanel() {
final ubicaciones = _getUbicacionesUnicas();
final componentesPorPiso = _agruparComponentesPorPiso();
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.4),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white.withOpacity(0.2)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Información del Plano',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
if (ubicaciones.isNotEmpty) ...[
Text(
'Ubicaciones detectadas: ${ubicaciones.length}',
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 14,
),
),
const SizedBox(height: 8),
...ubicaciones.take(4).map((ubicacion) {
final componentesEnUbicacion = provider.componentesTopologia
.where((c) => c.ubicacion == ubicacion)
.length;
return Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Text(
'$ubicacion ($componentesEnUbicacion componentes)',
style: TextStyle(
color: Colors.white.withOpacity(0.7),
fontSize: 12,
),
),
);
}),
if (ubicaciones.length > 4)
Text(
'... y ${ubicaciones.length - 4} más',
style: TextStyle(
color: Colors.white.withOpacity(0.5),
fontSize: 11,
),
),
] else ...[
Text(
'No se encontraron ubicaciones específicas',
style: TextStyle(
color: Colors.white.withOpacity(0.7),
fontSize: 14,
),
),
],
if (componentesPorPiso.isNotEmpty) ...[
const SizedBox(height: 12),
const Text(
'Distribución por niveles:',
style: TextStyle(
color: Colors.cyan,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 6),
...componentesPorPiso.entries.take(3).map((entry) => Padding(
padding: const EdgeInsets.only(bottom: 2),
child: Text(
'${entry.key}: ${entry.value} componentes',
style: TextStyle(
color: Colors.white.withOpacity(0.6),
fontSize: 11,
),
),
)),
],
const SizedBox(height: 12),
const Text(
'Funcionalidades planificadas:',
style: TextStyle(
color: Colors.cyan,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 6),
...[
'• Mapa interactivo de ubicaciones',
'• Vista por pisos y áreas',
'• Trazado de rutas de cableado',
'• Ubicación GPS de componentes',
].map((feature) => Padding(
padding: const EdgeInsets.only(bottom: 2),
child: Text(
feature,
style: TextStyle(
color: Colors.white.withOpacity(0.6),
fontSize: 11,
),
),
)),
],
),
).animate().fadeIn(delay: 700.ms).slideY(begin: 0.3);
}
Map<String, int> _agruparComponentesPorPiso() {
Map<String, int> pisos = {};
for (var componente in provider.componentesTopologia) {
if (componente.ubicacion != null) {
String ubicacion = componente.ubicacion!.toLowerCase();
String piso = 'Otros';
if (ubicacion.contains('piso') || ubicacion.contains('planta')) {
// Extraer número de piso
RegExp regex = RegExp(r'(piso|planta)\s*(\d+)', caseSensitive: false);
var match = regex.firstMatch(ubicacion);
if (match != null) {
piso = 'Piso ${match.group(2)}';
}
} else if (ubicacion.contains('pb') ||
ubicacion.contains('planta baja')) {
piso = 'Planta Baja';
} else if (ubicacion.contains('sotano') ||
ubicacion.contains('sótano')) {
piso = 'Sótano';
} else if (ubicacion.contains('azotea') ||
ubicacion.contains('terraza')) {
piso = 'Azotea';
}
pisos[piso] = (pisos[piso] ?? 0) + 1;
}
}
return pisos;
}
}

View File

@@ -1,863 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:nethive_neo/helpers/constants.dart';
import 'package:nethive_neo/providers/nethive/componentes_provider.dart';
import 'package:nethive_neo/models/nethive/rack_con_componentes_model.dart';
class RackViewWidget extends StatelessWidget {
final bool isMediumScreen;
final ComponentesProvider provider;
const RackViewWidget({
Key? key,
required this.isMediumScreen,
required this.provider,
}) : super(key: key);
@override
Widget build(BuildContext context) {
if (provider.isLoadingRacks) {
return _buildLoadingView();
}
if (provider.racksConComponentes.isEmpty) {
return _buildEmptyView();
}
return Container(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header con estadísticas
_buildRackSummaryHeader(),
const SizedBox(height: 24),
// Vista principal de racks
Expanded(
child: isMediumScreen
? _buildDesktopRackView()
: _buildMobileRackView(),
),
],
),
);
}
Widget _buildLoadingView() {
return Container(
padding: const EdgeInsets.all(24),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(
color: Colors.white,
strokeWidth: 3,
).animate().scale(duration: 600.ms),
const SizedBox(height: 20),
const Text(
'Cargando vista de racks...',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w600,
),
).animate().fadeIn(delay: 300.ms),
const SizedBox(height: 8),
const Text(
'Obteniendo componentes de cada rack',
style: TextStyle(
color: Colors.white70,
fontSize: 14,
),
).animate().fadeIn(delay: 500.ms),
],
),
),
);
}
Widget _buildEmptyView() {
return Container(
padding: const EdgeInsets.all(24),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.dns,
size: 80,
color: Colors.white.withOpacity(0.5),
).animate().scale(duration: 600.ms),
const SizedBox(height: 20),
const Text(
'Sin Racks Detectados',
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
).animate().fadeIn(delay: 300.ms),
const SizedBox(height: 8),
const Text(
'No se encontraron racks registrados\nen este negocio',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white70,
fontSize: 16,
),
).animate().fadeIn(delay: 500.ms),
const SizedBox(height: 24),
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.4),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white.withOpacity(0.2)),
),
child: Column(
children: [
const Text(
'Para ver racks aquí:',
style: TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
const Text(
'1. Cree componentes de tipo "Rack"',
style: TextStyle(color: Colors.white70, fontSize: 12),
),
const SizedBox(height: 4),
const Text(
'2. Asigne otros componentes a los racks',
style: TextStyle(color: Colors.white70, fontSize: 12),
),
const SizedBox(height: 4),
const Text(
'3. Configure posiciones U si es necesario',
style: TextStyle(color: Colors.white70, fontSize: 12),
),
],
),
).animate().fadeIn(delay: 700.ms),
],
),
),
);
}
Widget _buildRackSummaryHeader() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.blue.withOpacity(0.8),
Colors.blue.withOpacity(0.6),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.blue.withOpacity(0.3),
blurRadius: 15,
offset: const Offset(0, 5),
),
],
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.dns,
color: Colors.white,
size: 24,
),
).animate().scale(duration: 600.ms),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Vista de Racks - ${provider.negocioSeleccionadoNombre}',
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
).animate().fadeIn(delay: 300.ms),
const SizedBox(height: 4),
Text(
'${provider.totalRacks} racks • ${provider.totalComponentesEnRacks} componentes • ${provider.porcentajeOcupacionPromedio.toStringAsFixed(1)}% ocupación promedio',
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 14,
),
).animate().fadeIn(delay: 500.ms),
],
),
),
if (provider.racksConProblemas.isNotEmpty)
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.orange.withOpacity(0.3),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.warning, color: Colors.white, size: 16),
const SizedBox(width: 8),
Text(
'${provider.racksConProblemas.length} alertas',
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
],
),
).animate().fadeIn(delay: 700.ms),
],
),
).animate().fadeIn().slideY(begin: -0.3, end: 0);
}
Widget _buildDesktopRackView() {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Lista de racks
Expanded(
flex: 2,
child: _buildRacksList(),
),
const SizedBox(width: 24),
// Panel de información
Expanded(
flex: 1,
child: _buildRackInfoPanel(),
),
],
);
}
Widget _buildMobileRackView() {
return _buildRacksList();
}
Widget _buildRacksList() {
return GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: isMediumScreen ? 2 : 1,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
childAspectRatio: isMediumScreen ? 1.2 : 1.5,
),
itemCount: provider.racksConComponentes.length,
itemBuilder: (context, index) {
final rack = provider.racksConComponentes[index];
return _buildRackCard(rack, index);
},
);
}
Widget _buildRackCard(RackConComponentes rack, int index) {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.black.withOpacity(0.8),
Colors.black.withOpacity(0.6),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: Colors.white.withOpacity(0.3),
width: 1.5,
),
boxShadow: [
BoxShadow(
color: Colors.blue.withOpacity(0.2),
blurRadius: 15,
offset: const Offset(0, 8),
),
BoxShadow(
color: Colors.black.withOpacity(0.4),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(16),
onTap: () => _showRackDetails(rack),
child: Padding(
padding: const EdgeInsets.all(20),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Imagen del rack más grande
Container(
width: 90,
height: 90,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.blue.withOpacity(0.4),
width: 2,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: _buildRackImage(rack),
),
),
const SizedBox(width: 20),
// Información principal
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header del rack
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Colors.blue.withOpacity(0.4),
width: 1,
),
),
child: const Icon(
Icons.dns,
color: Colors.blue,
size: 18,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
rack.nombreRack,
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (rack.ubicacionRack != null)
Text(
rack.ubicacionRack!,
style: TextStyle(
color: Colors.white.withOpacity(0.7),
fontSize: 14,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
const SizedBox(height: 16),
// Estadísticas mejoradas
Row(
children: [
_buildEnhancedStatItem(
rack.cantidadComponentes.toString(),
'Total',
Colors.blue,
Icons.memory,
),
const SizedBox(width: 12),
_buildEnhancedStatItem(
rack.componentesActivos.toString(),
'Activos',
Colors.green,
Icons.check_circle,
),
const SizedBox(width: 12),
_buildEnhancedStatItem(
'${rack.porcentajeOcupacion.toStringAsFixed(0)}%',
'Ocupación',
rack.porcentajeOcupacion > 80
? Colors.red
: rack.porcentajeOcupacion > 60
? Colors.orange
: Colors.green,
Icons.dashboard,
),
],
),
const SizedBox(height: 16),
// Barra de ocupación mejorada
_buildEnhancedOccupationBar(rack),
const SizedBox(height: 12),
// Componentes preview mejorado
if (rack.componentes.isNotEmpty)
_buildEnhancedComponentsPreview(rack),
],
),
),
],
),
),
),
),
).animate().fadeIn(delay: (100 * index).ms).slideY(begin: 0.3);
}
Widget _buildRackImage(RackConComponentes rack) {
// Buscar la imagen del rack en los componentes
final rackComponent = provider.componentesTopologia
.where((c) => c.id == rack.rackId)
.firstOrNull;
final imagenUrl = rackComponent?.imagenUrl;
if (imagenUrl != null && imagenUrl.isNotEmpty) {
// Construir URL completa de Supabase
final fullImageUrl =
"$supabaseUrl/storage/v1/object/public/nethive/componentes/$imagenUrl?${DateTime.now().millisecondsSinceEpoch}";
return Image.network(
fullImageUrl,
fit: BoxFit.cover,
width: 90,
height: 90,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Container(
width: 90,
height: 90,
decoration: BoxDecoration(
color: Colors.grey.withOpacity(0.3),
borderRadius: BorderRadius.circular(10),
),
child: Center(
child: CircularProgressIndicator(
color: Colors.blue,
strokeWidth: 2,
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
),
);
},
errorBuilder: (context, error, stackTrace) {
return Image.asset(
'assets/images/placeholder_no_image.jpg',
fit: BoxFit.cover,
width: 90,
height: 90,
);
},
);
} else {
// Usar imagen placeholder local
return Image.asset(
'assets/images/placeholder_no_image.jpg',
fit: BoxFit.cover,
width: 90,
height: 90,
);
}
}
Widget _buildEnhancedStatItem(
String value, String label, Color color, IconData icon) {
return Expanded(
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
color.withOpacity(0.2),
color.withOpacity(0.1),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color.withOpacity(0.4), width: 1),
boxShadow: [
BoxShadow(
color: color.withOpacity(0.2),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
color: color,
size: 20,
),
const SizedBox(height: 4),
Text(
value,
style: TextStyle(
color: color,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
Text(
label,
style: TextStyle(
color: color.withOpacity(0.8),
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
],
),
),
);
}
Widget _buildEnhancedOccupationBar(RackConComponentes rack) {
final ocupacion = rack.porcentajeOcupacion;
final color = ocupacion > 80
? Colors.red
: ocupacion > 60
? Colors.orange
: Colors.green;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Ocupación del Rack',
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: color.withOpacity(0.2),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: color.withOpacity(0.4)),
),
child: Text(
'${ocupacion.toStringAsFixed(1)}%',
style: TextStyle(
color: color,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
],
),
const SizedBox(height: 8),
Container(
height: 8,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(4),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: ocupacion / 100,
backgroundColor: Colors.transparent,
valueColor: AlwaysStoppedAnimation<Color>(color),
),
),
),
],
);
}
Widget _buildEnhancedComponentsPreview(RackConComponentes rack) {
final componentesOrdenados = rack.componentesOrdenadosPorPosicion;
final maxPreview = 3;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.list_alt,
color: Colors.white.withOpacity(0.8),
size: 16,
),
const SizedBox(width: 6),
Text(
'Componentes principales:',
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: 8),
...componentesOrdenados.take(maxPreview).map((comp) => Container(
margin: const EdgeInsets.only(bottom: 6),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.05),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: comp.colorEstado.withOpacity(0.3),
width: 1,
),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: comp.colorEstado.withOpacity(0.2),
borderRadius: BorderRadius.circular(4),
),
child: Icon(
comp.iconoCategoria,
size: 14,
color: comp.colorEstado,
),
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
comp.nombre,
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 12,
fontWeight: FontWeight.w500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (comp.posicionU != null)
Text(
'Posición U${comp.posicionU}',
style: TextStyle(
color: Colors.white.withOpacity(0.6),
fontSize: 10,
),
),
],
),
),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: comp.colorEstado.withOpacity(0.2),
borderRadius: BorderRadius.circular(4),
),
child: Text(
comp.estadoTexto,
style: TextStyle(
color: comp.colorEstado,
fontSize: 9,
fontWeight: FontWeight.w600,
),
),
),
],
),
)),
if (componentesOrdenados.length > maxPreview)
Container(
margin: const EdgeInsets.only(top: 4),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: Colors.blue.withOpacity(0.3)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.more_horiz,
color: Colors.blue,
size: 14,
),
const SizedBox(width: 4),
Text(
'+${componentesOrdenados.length - maxPreview} componentes más',
style: const TextStyle(
color: Colors.blue,
fontSize: 11,
fontWeight: FontWeight.w500,
),
),
],
),
),
],
);
}
Widget _buildRackInfoPanel() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.4),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white.withOpacity(0.2)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Resumen de Racks',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
_buildInfoRow('Total de Racks', provider.totalRacks.toString()),
_buildInfoRow('Componentes Totales',
provider.totalComponentesEnRacks.toString()),
_buildInfoRow(
'Racks Activos', provider.racksConComponentesActivos.toString()),
_buildInfoRow('Ocupación Promedio',
'${provider.porcentajeOcupacionPromedio.toStringAsFixed(1)}%'),
if (provider.racksConProblemas.isNotEmpty) ...[
const SizedBox(height: 16),
const Divider(color: Colors.white24),
const SizedBox(height: 16),
const Text(
'Racks con Alertas',
style: TextStyle(
color: Colors.orange,
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
...provider.racksConProblemas.map((rack) => Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Text(
'${rack.nombreRack}',
style: TextStyle(
color: Colors.white.withOpacity(0.7),
fontSize: 12,
),
),
)),
],
const Spacer(),
const Text(
'Funcionalidades disponibles:',
style: TextStyle(
color: Colors.cyan,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 6),
...[
'• Vista detallada de cada rack',
'• Gestión de posiciones U',
'• Estados de componentes',
'• Alertas de ocupación',
].map((feature) => Padding(
padding: const EdgeInsets.only(bottom: 2),
child: Text(
feature,
style: TextStyle(
color: Colors.white.withOpacity(0.6),
fontSize: 11,
),
),
)),
],
),
).animate().fadeIn(delay: 800.ms).slideX(begin: 0.3);
}
Widget _buildInfoRow(String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 12,
),
),
Text(
value,
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
],
),
);
}
void _showRackDetails(RackConComponentes rack) {
// TODO: Implementar modal con detalles completos del rack
print('Mostrar detalles del rack: ${rack.nombreRack}');
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,741 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:nethive_neo/providers/nethive/componentes_provider.dart';
import 'package:nethive_neo/pages/infrastructure/widgets/edit_componente_dialog.dart';
import 'package:nethive_neo/theme/theme.dart';
class ComponentesCardsView extends StatefulWidget {
const ComponentesCardsView({Key? key}) : super(key: key);
@override
State<ComponentesCardsView> createState() => _ComponentesCardsViewState();
}
class _ComponentesCardsViewState extends State<ComponentesCardsView>
with TickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
_animationController.forward();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return FadeTransition(
opacity: _fadeAnimation,
child: Consumer<ComponentesProvider>(
builder: (context, componentesProvider, child) {
if (componentesProvider.componentes.isEmpty) {
return _buildEmptyState();
}
return Container(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header con filtros
_buildMobileHeader(componentesProvider),
const SizedBox(height: 16),
// Lista de tarjetas
Expanded(
child: ListView.builder(
itemCount: componentesProvider.componentes.length,
itemBuilder: (context, index) {
final componente = componentesProvider.componentes[index];
return TweenAnimationBuilder<double>(
duration: Duration(milliseconds: 300 + (index * 100)),
tween: Tween(begin: 0.0, end: 1.0),
builder: (context, value, child) {
return Transform.translate(
offset: Offset(0, 30 * (1 - value)),
child: Opacity(
opacity: value,
child: _buildComponenteCard(
componente, componentesProvider),
),
);
},
);
},
),
),
],
),
);
},
),
);
}
Widget _buildMobileHeader(ComponentesProvider componentesProvider) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: AppTheme.of(context).primaryGradient,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: AppTheme.of(context).primaryColor.withOpacity(0.3),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.inventory_2,
color: Colors.white,
size: 20,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Inventario MDF/IDF',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text(
'${componentesProvider.componentes.length} componentes',
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 12,
),
),
],
),
),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: const Text(
'Móvil',
style: TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
],
),
const SizedBox(height: 12),
// Buscador móvil
Container(
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
),
child: TextField(
style: const TextStyle(color: Colors.white),
decoration: InputDecoration(
hintText: 'Buscar componentes...',
hintStyle: TextStyle(color: Colors.white.withOpacity(0.7)),
prefixIcon: Icon(
Icons.search,
color: Colors.white.withOpacity(0.8),
size: 20,
),
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
onChanged: (value) {
componentesProvider.buscarComponentes(value);
},
),
),
],
),
);
}
Widget _buildComponenteCard(
dynamic componente, ComponentesProvider componentesProvider) {
// Buscar la categoría del componente
final categoria = componentesProvider.categorias
.where((cat) => cat.id == componente.categoriaId)
.firstOrNull;
return Container(
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: AppTheme.of(context).secondaryBackground,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppTheme.of(context).primaryColor.withOpacity(0.2),
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
_showComponenteDetails(componente, categoria);
},
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header de la tarjeta
Row(
children: [
// Imagen del componente
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color:
AppTheme.of(context).primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: componente.imagenUrl != null &&
componente.imagenUrl!.isNotEmpty
? ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
componente.imagenUrl!,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Icon(
Icons.devices,
color: AppTheme.of(context).primaryColor,
size: 24,
);
},
),
)
: Icon(
Icons.devices,
color: AppTheme.of(context).primaryColor,
size: 24,
),
),
const SizedBox(width: 12),
// Info principal
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
componente.nombre,
style: TextStyle(
color: AppTheme.of(context).primaryText,
fontSize: 16,
fontWeight: FontWeight.bold,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
if (categoria != null)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: AppTheme.of(context)
.primaryColor
.withOpacity(0.1),
borderRadius: BorderRadius.circular(6),
),
child: Text(
categoria.nombre,
style: TextStyle(
color: AppTheme.of(context).primaryColor,
fontSize: 10,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
// Estados
Column(
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color:
(componente.activo ? Colors.green : Colors.red)
.withOpacity(0.1),
borderRadius: BorderRadius.circular(6),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
componente.activo
? Icons.check_circle
: Icons.cancel,
color: componente.activo
? Colors.green
: Colors.red,
size: 10,
),
const SizedBox(width: 2),
Text(
componente.activo ? 'Activo' : 'Inactivo',
style: TextStyle(
color: componente.activo
? Colors.green
: Colors.red,
fontSize: 9,
fontWeight: FontWeight.bold,
),
),
],
),
),
const SizedBox(height: 4),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color:
(componente.enUso ? Colors.orange : Colors.grey)
.withOpacity(0.1),
borderRadius: BorderRadius.circular(6),
),
child: Text(
componente.enUso ? 'En Uso' : 'Libre',
style: TextStyle(
color: componente.enUso
? Colors.orange
: Colors.grey,
fontSize: 9,
fontWeight: FontWeight.bold,
),
),
),
],
),
],
),
const SizedBox(height: 12),
// Información adicional
if (componente.descripcion != null &&
componente.descripcion!.isNotEmpty)
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppTheme.of(context).tertiaryBackground,
borderRadius: BorderRadius.circular(6),
),
child: Text(
componente.descripcion!,
style: TextStyle(
color: AppTheme.of(context).secondaryText,
fontSize: 12,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(height: 8),
// Footer con ubicación y acciones
Row(
children: [
if (componente.ubicacion != null &&
componente.ubicacion!.isNotEmpty) ...[
Icon(
Icons.location_on,
color: AppTheme.of(context).primaryColor,
size: 14,
),
const SizedBox(width: 4),
Expanded(
child: Text(
componente.ubicacion!,
style: TextStyle(
color: AppTheme.of(context).secondaryText,
fontSize: 11,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
] else
Expanded(
child: Text(
'Sin ubicación específica',
style: TextStyle(
color: AppTheme.of(context).secondaryText,
fontSize: 11,
fontStyle: FontStyle.italic,
),
),
),
// Botones de acción
Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildActionButton(
icon: Icons.visibility,
color: Colors.blue,
onTap: () =>
_showComponenteDetails(componente, categoria),
),
const SizedBox(width: 4),
_buildActionButton(
icon: Icons.edit,
color: AppTheme.of(context).primaryColor,
onTap: () {
_showEditComponenteDialog(componente);
},
),
const SizedBox(width: 4),
_buildActionButton(
icon: Icons.delete,
color: Colors.red,
onTap: () => _confirmDelete(componente),
),
],
),
],
),
],
),
),
),
),
);
}
Widget _buildActionButton({
required IconData icon,
required Color color,
required VoidCallback onTap,
}) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(4),
child: Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: Icon(
icon,
color: color,
size: 14,
),
),
);
}
Widget _buildEmptyState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppTheme.of(context).primaryColor.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
Icons.inventory_2,
color: AppTheme.of(context).primaryColor,
size: 48,
),
),
const SizedBox(height: 16),
Text(
'No hay componentes',
style: TextStyle(
color: AppTheme.of(context).primaryText,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
'No se encontraron componentes\npara este negocio',
textAlign: TextAlign.center,
style: TextStyle(
color: AppTheme.of(context).secondaryText,
fontSize: 14,
),
),
],
),
);
}
void _showComponenteDetails(dynamic componente, dynamic categoria) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => Container(
height: MediaQuery.of(context).size.height * 0.8,
decoration: BoxDecoration(
color: AppTheme.of(context).primaryBackground,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
),
child: Column(
children: [
// Handle
Container(
margin: const EdgeInsets.only(top: 8),
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.grey.withOpacity(0.5),
borderRadius: BorderRadius.circular(2),
),
),
// Header
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: AppTheme.of(context).primaryGradient,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
),
child: Row(
children: [
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: componente.imagenUrl != null &&
componente.imagenUrl!.isNotEmpty
? ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(
componente.imagenUrl!,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return const Icon(
Icons.devices,
color: Colors.white,
size: 30,
);
},
),
)
: const Icon(
Icons.devices,
color: Colors.white,
size: 30,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
componente.nombre,
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
if (categoria != null)
Text(
categoria.nombre,
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 14,
),
),
],
),
),
IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.close, color: Colors.white),
),
],
),
),
// Contenido
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDetailRow(
'ID', componente.id.substring(0, 8) + '...'),
_buildDetailRow(
'Estado', componente.activo ? 'Activo' : 'Inactivo'),
_buildDetailRow('En Uso', componente.enUso ? '' : 'No'),
if (componente.ubicacion != null &&
componente.ubicacion!.isNotEmpty)
_buildDetailRow('Ubicación', componente.ubicacion!),
if (componente.descripcion != null &&
componente.descripcion!.isNotEmpty)
_buildDetailRow('Descripción', componente.descripcion!),
_buildDetailRow(
'Fecha de Registro',
componente.fechaRegistro?.toString().split(' ')[0] ??
'No disponible'),
],
),
),
),
],
),
),
);
}
Widget _buildDetailRow(String label, String value) {
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppTheme.of(context).secondaryBackground,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: AppTheme.of(context).primaryColor.withOpacity(0.2),
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 100,
child: Text(
label,
style: TextStyle(
color: AppTheme.of(context).primaryColor,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
Expanded(
child: Text(
value,
style: TextStyle(
color: AppTheme.of(context).primaryText,
fontSize: 12,
),
),
),
],
),
);
}
void _confirmDelete(dynamic componente) {
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: AppTheme.of(context).primaryBackground,
title: const Text('Eliminar Componente'),
content: Text('¿Deseas eliminar "${componente.nombre}"?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
'Cancelar',
style: TextStyle(color: AppTheme.of(context).secondaryText),
),
),
TextButton(
onPressed: () {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Eliminar próximamente')),
);
},
child: const Text('Eliminar', style: TextStyle(color: Colors.red)),
),
],
),
);
}
void _showEditComponenteDialog(dynamic componente) {
final provider = Provider.of<ComponentesProvider>(context, listen: false);
showDialog(
context: context,
builder: (context) => EditComponenteDialog(
provider: provider,
componente: componente,
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,463 +0,0 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:provider/provider.dart';
import 'package:go_router/go_router.dart';
import 'package:nethive_neo/providers/nethive/navigation_provider.dart';
import 'package:nethive_neo/theme/theme.dart';
class InfrastructureSidemenu extends StatefulWidget {
final bool isExpanded;
final VoidCallback onToggle;
const InfrastructureSidemenu({
Key? key,
required this.isExpanded,
required this.onToggle,
}) : super(key: key);
@override
State<InfrastructureSidemenu> createState() => _InfrastructureSidemenuState();
}
class _InfrastructureSidemenuState extends State<InfrastructureSidemenu>
with TickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
_animationController.forward();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return FadeTransition(
opacity: _fadeAnimation,
child: Consumer<NavigationProvider>(
builder: (context, navigationProvider, child) {
return Container(
width: widget.isExpanded ? 280 : 80,
decoration: BoxDecoration(
gradient: AppTheme.of(context).darkBackgroundGradient,
border: Border(
right: BorderSide(
color: AppTheme.of(context).primaryColor.withOpacity(0.2),
width: 1,
),
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(2, 0),
),
],
),
child: Column(
children: [
// Header con logo y toggle
_buildHeader(navigationProvider),
// Información del negocio seleccionado
if (widget.isExpanded &&
navigationProvider.negocioSeleccionado != null)
_buildBusinessInfo(navigationProvider),
// Lista de opciones del menú
Expanded(
child: _buildMenuItems(navigationProvider),
),
// Footer con información adicional
if (widget.isExpanded) _buildFooter(),
],
),
);
},
),
);
}
Widget _buildHeader(NavigationProvider navigationProvider) {
return Container(
padding: EdgeInsets.all(widget.isExpanded ? 20 : 15),
decoration: BoxDecoration(
gradient: AppTheme.of(context).primaryGradient,
boxShadow: [
BoxShadow(
color: AppTheme.of(context).primaryColor.withOpacity(0.3),
blurRadius: 15,
offset: const Offset(0, 5),
),
],
),
child: Row(
children: [
// Toggle button
GestureDetector(
onTap: widget.onToggle,
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
widget.isExpanded ? Icons.menu_open : Icons.menu,
color: Colors.white,
size: 24,
),
),
),
if (widget.isExpanded) ...[
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ShaderMask(
shaderCallback: (bounds) => LinearGradient(
colors: [Colors.white, Colors.white.withOpacity(0.8)],
).createShader(bounds),
child: Row(
children: [
Image.asset(
'assets/images/favicon.png',
width: 32,
height: 32,
),
const Gap(8),
const Text(
'NETHIVE',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
letterSpacing: 1.2,
),
),
],
),
),
Text(
'Infraestructura',
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
],
),
),
],
],
),
);
}
Widget _buildBusinessInfo(NavigationProvider navigationProvider) {
final negocio = navigationProvider.negocioSeleccionado!;
final empresa = navigationProvider.empresaSeleccionada!;
return Container(
margin: const EdgeInsets.all(16),
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),
],
),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppTheme.of(context).primaryColor.withOpacity(0.3),
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.circular(6),
),
child: const Icon(
Icons.business_center,
color: Colors.white,
size: 16,
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
empresa.nombre,
style: TextStyle(
color: AppTheme.of(context).primaryColor,
fontSize: 12,
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 8),
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.green.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 3),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
negocio.nombre,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.bold,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
'(${negocio.tipoLocal})',
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 11,
fontWeight: FontWeight.w500,
),
),
],
),
),
],
),
);
}
Widget _buildMenuItems(NavigationProvider navigationProvider) {
return ListView.builder(
padding: EdgeInsets.symmetric(
vertical: 8,
horizontal: widget.isExpanded ? 16 : 8,
),
itemCount: navigationProvider.menuItems.length,
itemBuilder: (context, index) {
final menuItem = navigationProvider.menuItems[index];
final isSelected =
navigationProvider.selectedMenuIndex == menuItem.index;
final isSpecial = menuItem.isSpecial;
return TweenAnimationBuilder<double>(
duration: Duration(milliseconds: 200 + (index * 50)),
tween: Tween(begin: 0.0, end: 1.0),
builder: (context, value, child) {
return Transform.translate(
offset: Offset(-30 * (1 - value), 0),
child: Opacity(
opacity: value,
child: _buildMenuItem(
menuItem,
isSelected,
isSpecial,
navigationProvider,
),
),
);
},
);
},
);
}
Widget _buildMenuItem(
NavigationMenuItem menuItem,
bool isSelected,
bool isSpecial,
NavigationProvider navigationProvider,
) {
return Container(
margin: const EdgeInsets.only(bottom: 4),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => _handleMenuTap(menuItem, navigationProvider),
borderRadius: BorderRadius.circular(12),
child: Container(
padding: EdgeInsets.all(widget.isExpanded ? 12 : 8),
decoration: BoxDecoration(
gradient: isSelected
? AppTheme.of(context).primaryGradient
: isSpecial
? LinearGradient(
colors: [
Colors.orange.withOpacity(0.1),
Colors.deepOrange.withOpacity(0.1),
],
)
: null,
borderRadius: BorderRadius.circular(12),
border: isSpecial
? Border.all(
color: Colors.orange.withOpacity(0.3),
width: 1,
)
: null,
),
child: Row(
children: [
Icon(
menuItem.icon,
color: isSelected
? Colors.white
: isSpecial
? Colors.orange
: AppTheme.of(context).primaryText,
size: 20,
),
if (widget.isExpanded) ...[
const SizedBox(width: 12),
Expanded(
child: Text(
menuItem.title,
style: TextStyle(
color: isSelected
? Colors.white
: isSpecial
? Colors.orange
: AppTheme.of(context).primaryText,
fontSize: 14,
fontWeight:
isSelected ? FontWeight.bold : FontWeight.w500,
),
),
),
if (isSelected)
Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
shape: BoxShape.circle,
),
child: const Icon(
Icons.arrow_forward_ios,
color: Colors.white,
size: 12,
),
),
],
],
),
),
),
),
);
}
Widget _buildFooter() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
AppTheme.of(context).primaryBackground.withOpacity(0.0),
AppTheme.of(context).primaryBackground,
],
),
),
child: Column(
children: [
Container(
height: 1,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.transparent,
AppTheme.of(context).primaryColor.withOpacity(0.5),
Colors.transparent,
],
),
),
),
const SizedBox(height: 16),
Row(
children: [
Icon(
Icons.shield_outlined,
color: AppTheme.of(context).primaryColor,
size: 16,
),
const SizedBox(width: 8),
Expanded(
child: Text(
'Conexión segura',
style: TextStyle(
color: AppTheme.of(context).primaryColor,
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
),
],
),
],
),
);
}
void _handleMenuTap(
NavigationMenuItem menuItem, NavigationProvider navigationProvider) {
if (menuItem.isSpecial) {
// Si es "Empresas", regresar a la página de empresas
navigationProvider.clearSelection();
context.go('/');
} else {
// Cambiar la selección del menú
navigationProvider.setSelectedMenuIndex(menuItem.index);
// Aquí puedes agregar navegación específica si es necesario
// Por ahora solo cambiaremos la vista en el layout principal
}
}
}

View File

@@ -1,569 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:go_router/go_router.dart';
import 'package:nethive_neo/providers/nethive/navigation_provider.dart';
import 'package:nethive_neo/theme/theme.dart';
class MobileNavigationModal extends StatefulWidget {
const MobileNavigationModal({Key? key}) : super(key: key);
@override
State<MobileNavigationModal> createState() => _MobileNavigationModalState();
}
class _MobileNavigationModalState extends State<MobileNavigationModal>
with TickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 400),
vsync: this,
);
_fadeAnimation = CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
);
_slideAnimation = Tween<Offset>(
begin: const Offset(0, -1),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeOutCubic,
));
_animationController.forward();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return FadeTransition(
opacity: _fadeAnimation,
child: Container(
decoration: BoxDecoration(
color: AppTheme.of(context).primaryBackground.withOpacity(0.95),
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(30),
bottomRight: Radius.circular(30),
),
),
child: Consumer<NavigationProvider>(
builder: (context, navigationProvider, child) {
return SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Header del modal
/* _buildModalHeader(navigationProvider), */
// Lista de opciones de navegación
_buildNavigationOptions(navigationProvider),
// Información del negocio
_buildBusinessInfo(navigationProvider),
// Botón para cerrar
_buildCloseButton(),
// Padding adicional para evitar overflow
const SizedBox(height: 20),
],
),
);
},
),
),
);
}
Widget _buildModalHeader(NavigationProvider navigationProvider) {
return SlideTransition(
position: _slideAnimation,
child: Container(
padding: const EdgeInsets.fromLTRB(24, 40, 24, 20),
decoration: BoxDecoration(
gradient: AppTheme.of(context).primaryGradient,
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(30),
bottomRight: Radius.circular(30),
),
boxShadow: [
BoxShadow(
color: AppTheme.of(context).primaryColor.withOpacity(0.3),
blurRadius: 15,
offset: const Offset(0, 5),
),
],
),
child: Row(
children: [
// Logo animado
TweenAnimationBuilder<double>(
duration: const Duration(milliseconds: 800),
tween: Tween(begin: 0.0, end: 1.0),
builder: (context, value, child) {
return Transform.scale(
scale: 0.8 + (0.2 * value),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: Colors.white.withOpacity(0.3),
width: 2,
),
),
child: Image.asset(
'assets/images/favicon.png',
width: 32,
height: 32,
),
),
);
},
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ShaderMask(
shaderCallback: (bounds) => LinearGradient(
colors: [Colors.white, Colors.white.withOpacity(0.8)],
).createShader(bounds),
child: const Text(
'NETHIVE',
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
letterSpacing: 1.5,
),
),
),
Text(
'Infraestructura MDF/IDF',
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
],
),
),
// Botón de cerrar
GestureDetector(
onTap: () => Navigator.of(context).pop(),
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(
Icons.close,
color: Colors.white,
size: 24,
),
),
),
],
),
),
);
}
Widget _buildNavigationOptions(NavigationProvider navigationProvider) {
return Container(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Título de sección
Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Row(
children: [
Icon(
Icons.navigation,
color: AppTheme.of(context).primaryColor,
size: 20,
),
const SizedBox(width: 8),
Text(
'Módulos de Infraestructura',
style: TextStyle(
color: AppTheme.of(context).primaryColor,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
),
),
// Lista de opciones
...navigationProvider.menuItems.asMap().entries.map((entry) {
final index = entry.key;
final menuItem = entry.value;
final isSelected =
navigationProvider.selectedMenuIndex == menuItem.index;
final isSpecial = menuItem.isSpecial;
return TweenAnimationBuilder<double>(
duration: Duration(milliseconds: 300 + (index * 100)),
tween: Tween(begin: 0.0, end: 1.0),
builder: (context, value, child) {
return Transform.translate(
offset: Offset(50 * (1 - value), 0),
child: Opacity(
opacity: value,
child: _buildNavigationItem(
menuItem,
isSelected,
isSpecial,
navigationProvider,
),
),
);
},
);
}).toList(),
],
),
);
}
Widget _buildNavigationItem(
NavigationMenuItem menuItem,
bool isSelected,
bool isSpecial,
NavigationProvider navigationProvider,
) {
return Container(
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
gradient: isSelected
? AppTheme.of(context).primaryGradient
: isSpecial
? LinearGradient(
colors: [
Colors.orange.withOpacity(0.1),
Colors.deepOrange.withOpacity(0.1),
],
)
: LinearGradient(
colors: [
AppTheme.of(context).secondaryBackground,
AppTheme.of(context).tertiaryBackground,
],
),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isSelected
? Colors.white.withOpacity(0.3)
: isSpecial
? Colors.orange.withOpacity(0.3)
: AppTheme.of(context).primaryColor.withOpacity(0.2),
width: isSelected ? 2 : 1,
),
boxShadow: [
BoxShadow(
color: isSelected
? AppTheme.of(context).primaryColor.withOpacity(0.3)
: Colors.black.withOpacity(0.1),
blurRadius: isSelected ? 15 : 8,
offset: Offset(0, isSelected ? 8 : 3),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => _handleMenuTap(menuItem, navigationProvider),
borderRadius: BorderRadius.circular(16),
child: Padding(
padding: const EdgeInsets.all(20),
child: Row(
children: [
// Icono del módulo
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isSelected
? Colors.white.withOpacity(0.2)
: isSpecial
? Colors.orange.withOpacity(0.2)
: AppTheme.of(context)
.primaryColor
.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
menuItem.icon,
color: isSelected
? Colors.white
: isSpecial
? Colors.orange
: AppTheme.of(context).primaryColor,
size: 24,
),
),
const SizedBox(width: 16),
// Información del módulo
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
menuItem.title,
style: TextStyle(
color: isSelected
? Colors.white
: isSpecial
? Colors.orange
: AppTheme.of(context).primaryText,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
_getMenuItemDescription(menuItem),
style: TextStyle(
color: isSelected
? Colors.white.withOpacity(0.8)
: isSpecial
? Colors.orange.withOpacity(0.8)
: AppTheme.of(context).secondaryText,
fontSize: 14,
),
),
],
),
),
// Indicador de selección
if (isSelected)
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
shape: BoxShape.circle,
),
child: const Icon(
Icons.check,
color: Colors.white,
size: 20,
),
)
else
Icon(
Icons.arrow_forward_ios,
color: isSpecial
? Colors.orange.withOpacity(0.6)
: AppTheme.of(context).secondaryText,
size: 16,
),
],
),
),
),
),
);
}
Widget _buildBusinessInfo(NavigationProvider navigationProvider) {
final negocio = navigationProvider.negocioSeleccionado;
final empresa = navigationProvider.empresaSeleccionada;
if (negocio == null || empresa == null) return const SizedBox.shrink();
return Container(
margin: const EdgeInsets.all(20),
padding: const EdgeInsets.all(20),
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),
],
),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: AppTheme.of(context).primaryColor.withOpacity(0.3),
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
gradient: AppTheme.of(context).primaryGradient,
borderRadius: BorderRadius.circular(10),
),
child: const Icon(
Icons.business_center,
color: Colors.white,
size: 20,
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
'Ubicación Actual',
style: TextStyle(
color: AppTheme.of(context).primaryColor,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
],
),
const SizedBox(height: 16),
// Información de la empresa
Text(
empresa.nombre,
style: TextStyle(
color: AppTheme.of(context).primaryText,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
// Información del negocio
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.circular(10),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.location_on,
color: Colors.white,
size: 16,
),
const SizedBox(width: 6),
Text(
negocio.nombre,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
],
),
),
],
),
);
}
Widget _buildCloseButton() {
return Container(
padding: const EdgeInsets.all(20),
child: SizedBox(
width: double.infinity,
child: Container(
decoration: BoxDecoration(
gradient: AppTheme.of(context).modernGradient,
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: AppTheme.of(context).primaryColor.withOpacity(0.3),
blurRadius: 15,
offset: const Offset(0, 5),
),
],
),
child: ElevatedButton.icon(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.close, color: Colors.white),
label: const Text(
'Cerrar Menú',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
),
),
),
),
);
}
String _getMenuItemDescription(NavigationMenuItem menuItem) {
switch (menuItem.title) {
case 'Dashboard':
return 'Métricas y estadísticas generales';
case 'Inventario':
return 'Gestión de componentes de red';
case 'Topología':
return 'Visualización de infraestructura';
case 'Alertas':
return 'Notificaciones del sistema';
case 'Configuración':
return 'Parámetros y ajustes';
case 'Empresas':
return 'Volver a gestión empresarial';
default:
return 'Módulo de infraestructura';
}
}
void _handleMenuTap(
NavigationMenuItem menuItem,
NavigationProvider navigationProvider,
) {
if (menuItem.isSpecial) {
// Si es "Empresas", regresar a la página de empresas
navigationProvider.clearSelection();
context.go('/');
} else {
// Cambiar la selección del menú
navigationProvider.setSelectedMenuIndex(menuItem.index);
}
// Cerrar el modal después de la selección
Navigator.of(context).pop();
}
}

View File

@@ -0,0 +1,536 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:nethive_neo/providers/videos_provider.dart';
import 'package:nethive_neo/theme/theme.dart';
import 'package:gap/gap.dart';
class DashboardPage extends StatefulWidget {
const DashboardPage({Key? key}) : super(key: key);
@override
State<DashboardPage> createState() => _DashboardPageState();
}
class _DashboardPageState extends State<DashboardPage>
with TickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
Map<String, dynamic> stats = {};
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
_animationController.forward();
_loadStats();
}
Future<void> _loadStats() async {
final provider = Provider.of<VideosProvider>(context, listen: false);
final result = await provider.getDashboardStats();
setState(() {
stats = result;
});
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final isMobile = MediaQuery.of(context).size.width <= 800;
return FadeTransition(
opacity: _fadeAnimation,
child: SingleChildScrollView(
padding: EdgeInsets.all(isMobile ? 16 : 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const Gap(24),
_buildStatsCards(isMobile),
const Gap(24),
if (!isMobile) ...[
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: _buildCategoryChart()),
const Gap(24),
Expanded(child: _buildRecentActivity()),
],
),
] else ...[
_buildCategoryChart(),
const Gap(24),
_buildRecentActivity(),
],
],
),
),
);
}
Widget _buildHeader() {
return Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
gradient: AppTheme.of(context).primaryGradient,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: AppTheme.of(context).primaryColor.withOpacity(0.3),
blurRadius: 20,
offset: const Offset(0, 5),
),
],
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.dashboard,
size: 32,
color: Colors.white,
),
),
const Gap(16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Dashboard MDF/IDF',
style: AppTheme.of(context).title1.override(
fontFamily: 'Poppins',
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const Gap(4),
Text(
'Panel de control de contenido multimedia',
style: AppTheme.of(context).bodyText1.override(
fontFamily: 'Poppins',
color: Colors.white.withOpacity(0.9),
fontSize: 14,
),
),
],
),
),
],
),
);
}
Widget _buildStatsCards(bool isMobile) {
return isMobile
? Column(
children: [
_buildStatCard(
'Videos Totales',
stats['total_videos']?.toString() ?? '0',
Icons.video_library,
AppTheme.of(context).primaryColor,
),
const Gap(16),
_buildStatCard(
'Reproducciones',
stats['total_reproducciones']?.toString() ?? '0',
Icons.play_circle_filled,
AppTheme.of(context).secondaryColor,
),
const Gap(16),
_buildStatCard(
'Categorías',
stats['total_categories']?.toString() ?? '0',
Icons.category,
AppTheme.of(context).tertiaryColor,
),
const Gap(16),
_buildStatCard(
'Video más visto',
stats['most_viewed_video']?['title'] ?? 'N/A',
Icons.trending_up,
AppTheme.of(context).error,
),
],
)
: Row(
children: [
Expanded(
child: _buildStatCard(
'Videos Totales',
stats['total_videos']?.toString() ?? '0',
Icons.video_library,
AppTheme.of(context).primaryColor,
),
),
const Gap(16),
Expanded(
child: _buildStatCard(
'Reproducciones',
stats['total_reproducciones']?.toString() ?? '0',
Icons.play_circle_filled,
AppTheme.of(context).secondaryColor,
),
),
const Gap(16),
Expanded(
child: _buildStatCard(
'Categorías',
stats['total_categories']?.toString() ?? '0',
Icons.category,
AppTheme.of(context).tertiaryColor,
),
),
const Gap(16),
Expanded(
child: _buildStatCard(
'Video más visto',
stats['most_viewed_video']?['title'] ?? 'N/A',
Icons.trending_up,
AppTheme.of(context).error,
),
),
],
);
}
Widget _buildStatCard(
String title, String value, IconData icon, Color color) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppTheme.of(context).secondaryBackground,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: color.withOpacity(0.3),
),
boxShadow: [
BoxShadow(
color: color.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
icon,
color: color,
size: 24,
),
),
],
),
const Gap(16),
Text(
value,
style: AppTheme.of(context).title1.override(
fontFamily: 'Poppins',
color: AppTheme.of(context).primaryText,
fontSize: 28,
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const Gap(4),
Text(
title,
style: AppTheme.of(context).bodyText2.override(
fontFamily: 'Poppins',
color: AppTheme.of(context).secondaryText,
fontSize: 14,
),
),
],
),
);
}
Widget _buildCategoryChart() {
final categoriesMap = stats['videos_by_category'] as Map<String, dynamic>?;
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppTheme.of(context).secondaryBackground,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: AppTheme.of(context).primaryColor.withOpacity(0.2),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.pie_chart,
color: AppTheme.of(context).primaryColor,
),
const Gap(8),
Text(
'Distribución por Categoría',
style: AppTheme.of(context).title3.override(
fontFamily: 'Poppins',
color: AppTheme.of(context).primaryText,
fontWeight: FontWeight.bold,
),
),
],
),
const Gap(20),
if (categoriesMap != null && categoriesMap.isNotEmpty)
...categoriesMap.entries.map(
(entry) => Padding(
padding: const EdgeInsets.only(bottom: 12),
child: _buildCategoryBar(
entry.key,
entry.value,
categoriesMap.values.reduce((a, b) => a > b ? a : b),
),
),
)
else
Center(
child: Padding(
padding: const EdgeInsets.all(40),
child: Text(
'No hay datos de categorías',
style: AppTheme.of(context).bodyText2.override(
fontFamily: 'Poppins',
color: AppTheme.of(context).tertiaryText,
),
),
),
),
],
),
);
}
Widget _buildCategoryBar(String category, int count, int maxCount) {
final percentage = maxCount > 0 ? count / maxCount : 0.0;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
category,
style: AppTheme.of(context).bodyText1.override(
fontFamily: 'Poppins',
color: AppTheme.of(context).primaryText,
),
),
Text(
count.toString(),
style: AppTheme.of(context).bodyText1.override(
fontFamily: 'Poppins',
color: AppTheme.of(context).primaryColor,
fontWeight: FontWeight.bold,
),
),
],
),
const Gap(8),
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: percentage,
backgroundColor: AppTheme.of(context).tertiaryBackground,
valueColor: AlwaysStoppedAnimation<Color>(
AppTheme.of(context).primaryColor,
),
minHeight: 8,
),
),
],
);
}
Widget _buildRecentActivity() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppTheme.of(context).secondaryBackground,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: AppTheme.of(context).primaryColor.withOpacity(0.2),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.access_time,
color: AppTheme.of(context).primaryColor,
),
const Gap(8),
Text(
'Actividad Reciente',
style: AppTheme.of(context).title3.override(
fontFamily: 'Poppins',
color: AppTheme.of(context).primaryText,
fontWeight: FontWeight.bold,
),
),
],
),
const Gap(20),
Consumer<VideosProvider>(
builder: (context, provider, child) {
if (provider.mediaFiles.isEmpty) {
return Center(
child: Padding(
padding: const EdgeInsets.all(40),
child: Text(
'No hay actividad reciente',
style: AppTheme.of(context).bodyText2.override(
fontFamily: 'Poppins',
color: AppTheme.of(context).tertiaryText,
),
),
),
);
}
final recentVideos = provider.mediaFiles.take(5).toList();
return Column(
children: recentVideos.map((video) {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: AppTheme.of(context)
.primaryColor
.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.video_library,
color: AppTheme.of(context).primaryColor,
),
),
const Gap(12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
video.title ?? video.fileName,
style: AppTheme.of(context).bodyText1.override(
fontFamily: 'Poppins',
color: AppTheme.of(context).primaryText,
fontWeight: FontWeight.w600,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const Gap(4),
Text(
'Hace ${_getTimeAgo(video.createdAt)}',
style: AppTheme.of(context).bodyText2.override(
fontFamily: 'Poppins',
color: AppTheme.of(context).tertiaryText,
fontSize: 12,
),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: AppTheme.of(context)
.secondaryColor
.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'${video.reproducciones} views',
style: AppTheme.of(context).bodyText2.override(
fontFamily: 'Poppins',
color: AppTheme.of(context).secondaryColor,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
],
),
);
}).toList(),
);
},
),
],
),
);
}
String _getTimeAgo(DateTime? date) {
if (date == null) return 'desconocido';
final now = DateTime.now();
final difference = now.difference(date);
if (difference.inDays > 7) {
return '${(difference.inDays / 7).floor()} semanas';
} else if (difference.inDays > 0) {
return '${difference.inDays} días';
} else if (difference.inHours > 0) {
return '${difference.inHours} horas';
} else if (difference.inMinutes > 0) {
return '${difference.inMinutes} minutos';
} else {
return 'hace un momento';
}
}
}

View File

@@ -0,0 +1,823 @@
import 'package:flutter/material.dart';
import 'package:pluto_grid/pluto_grid.dart';
import 'package:provider/provider.dart';
import 'package:nethive_neo/providers/videos_provider.dart';
import 'package:nethive_neo/models/media/media_models.dart';
import 'package:nethive_neo/theme/theme.dart';
import 'package:nethive_neo/helpers/globals.dart';
import 'package:nethive_neo/widgets/premium_button.dart';
import 'package:nethive_neo/pages/videos/widgets/premium_upload_dialog.dart';
import 'package:gap/gap.dart';
class GestorVideosPage extends StatefulWidget {
const GestorVideosPage({Key? key}) : super(key: key);
@override
State<GestorVideosPage> createState() => _GestorVideosPageState();
}
class _GestorVideosPageState extends State<GestorVideosPage> {
PlutoGridStateManager? _stateManager;
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadData();
}
Future<void> _loadData() async {
setState(() => _isLoading = true);
final provider = Provider.of<VideosProvider>(context, listen: false);
await Future.wait([
provider.loadMediaFiles(),
provider.loadCategories(),
]);
setState(() => _isLoading = false);
}
@override
Widget build(BuildContext context) {
final isMobile = MediaQuery.of(context).size.width <= 800;
if (_isLoading) {
return Center(
child: CircularProgressIndicator(
color: AppTheme.of(context).primaryColor,
),
);
}
return Consumer<VideosProvider>(
builder: (context, provider, child) {
if (isMobile) {
return _buildMobileView(provider);
} else {
return _buildDesktopView(provider);
}
},
);
}
Widget _buildDesktopView(VideosProvider provider) {
return Column(
children: [
_buildToolbar(provider, false),
Expanded(
child: Padding(
padding: const EdgeInsets.all(24),
child: _buildPlutoGrid(provider),
),
),
],
);
}
Widget _buildMobileView(VideosProvider provider) {
return Column(
children: [
_buildToolbar(provider, true),
Expanded(
child: provider.mediaFiles.isEmpty
? _buildEmptyState()
: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: provider.mediaFiles.length,
itemBuilder: (context, index) {
final video = provider.mediaFiles[index];
return _buildVideoCard(video, provider);
},
),
),
],
);
}
Widget _buildToolbar(VideosProvider provider, bool isMobile) {
return Container(
padding: EdgeInsets.all(isMobile ? 16 : 24),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
AppTheme.of(context).primaryBackground,
AppTheme.of(context).secondaryBackground,
],
),
border: Border(
bottom: BorderSide(
color: AppTheme.of(context).primaryColor.withOpacity(0.1),
width: 1,
),
),
boxShadow: [
BoxShadow(
color: AppTheme.of(context).primaryColor.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
const Color(0xFF4EC9F5),
const Color(0xFFFFB733),
],
),
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: AppTheme.of(context).primaryColor.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: const Icon(
Icons.video_library,
color: Color(0xFF0B0B0D),
size: 24,
),
),
const Gap(16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Gestor de Videos',
style: AppTheme.of(context).title2.override(
fontFamily: 'Poppins',
color: AppTheme.of(context).primaryText,
fontWeight: FontWeight.bold,
fontSize: 22,
),
),
if (!isMobile) ...[
const Gap(4),
Text(
'${provider.mediaFiles.length} videos disponibles',
style: AppTheme.of(context).bodyText2.override(
fontFamily: 'Poppins',
color: AppTheme.of(context).tertiaryText,
fontSize: 13,
),
),
],
],
),
),
PremiumButton(
text: isMobile ? 'Subir' : 'Subir Video',
icon: Icons.cloud_upload,
onPressed: () => _showUploadDialog(provider),
width: isMobile ? 100 : null,
),
],
),
const Gap(16),
_buildSearchField(provider),
],
),
);
}
Widget _buildSearchField(VideosProvider provider) {
return Container(
decoration: BoxDecoration(
color: AppTheme.of(context).tertiaryBackground,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppTheme.of(context).primaryColor.withOpacity(0.1),
),
),
child: TextField(
controller: provider.busquedaVideoController,
style: AppTheme.of(context).bodyText1.override(
fontFamily: 'Poppins',
color: AppTheme.of(context).primaryText,
),
decoration: InputDecoration(
hintText: 'Buscar videos por título o descripción...',
hintStyle: AppTheme.of(context).bodyText1.override(
fontFamily: 'Poppins',
color: AppTheme.of(context).tertiaryText,
),
prefixIcon: Icon(
Icons.search,
color: AppTheme.of(context).primaryColor,
),
suffixIcon: provider.busquedaVideoController.text.isNotEmpty
? IconButton(
icon: Icon(
Icons.clear,
color: AppTheme.of(context).tertiaryText,
),
onPressed: () {
provider.busquedaVideoController.clear();
provider.searchVideos('');
},
)
: null,
border: InputBorder.none,
contentPadding: const EdgeInsets.all(16),
),
onChanged: (value) => provider.searchVideos(value),
),
);
}
Widget _buildPlutoGrid(VideosProvider provider) {
final columns = [
PlutoColumn(
title: 'Vista Previa',
field: 'thumbnail',
type: PlutoColumnType.text(),
width: 120,
enableColumnDrag: false,
enableSorting: false,
enableContextMenu: false,
renderer: (rendererContext) {
final video =
rendererContext.row.cells['video']?.value as MediaFileModel?;
if (video == null) return const SizedBox();
return Container(
margin: const EdgeInsets.all(4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: AppTheme.of(context).tertiaryBackground,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: video.fileUrl != null
? Image.network(
video.fileUrl!,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) => Icon(
Icons.video_library,
size: 32,
color: AppTheme.of(context).tertiaryText,
),
)
: Icon(
Icons.video_library,
size: 32,
color: AppTheme.of(context).tertiaryText,
),
),
);
},
),
PlutoColumn(
title: 'Título',
field: 'title',
type: PlutoColumnType.text(),
width: 250,
),
PlutoColumn(
title: 'Archivo',
field: 'fileName',
type: PlutoColumnType.text(),
width: 200,
),
PlutoColumn(
title: 'Categoría',
field: 'category',
type: PlutoColumnType.text(),
width: 150,
),
PlutoColumn(
title: 'Reproducciones',
field: 'reproducciones',
type: PlutoColumnType.number(),
width: 120,
textAlign: PlutoColumnTextAlign.center,
),
PlutoColumn(
title: 'Duración',
field: 'duration',
type: PlutoColumnType.text(),
width: 100,
),
PlutoColumn(
title: 'Fecha de Creación',
field: 'createdAt',
type: PlutoColumnType.text(),
width: 150,
),
PlutoColumn(
title: 'Acciones',
field: 'actions',
type: PlutoColumnType.text(),
width: 140,
enableColumnDrag: false,
enableSorting: false,
enableContextMenu: false,
renderer: (rendererContext) {
final video =
rendererContext.row.cells['video']?.value as MediaFileModel?;
if (video == null) return const SizedBox();
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: const Icon(Icons.play_circle_outline, size: 20),
color: const Color(0xFF4EC9F5),
tooltip: 'Reproducir',
onPressed: () => _playVideo(video),
),
IconButton(
icon: const Icon(Icons.edit, size: 20),
color: const Color(0xFFFFB733),
tooltip: 'Editar',
onPressed: () => _editVideo(video, provider),
),
IconButton(
icon: const Icon(Icons.delete, size: 20),
color: const Color(0xFFFF2D2D),
tooltip: 'Eliminar',
onPressed: () => _deleteVideo(video, provider),
),
],
);
},
),
];
return PlutoGrid(
columns: columns,
rows: provider.videosRows,
onLoaded: (PlutoGridOnLoadedEvent event) {
_stateManager = event.stateManager;
_stateManager!.setShowColumnFilter(true);
},
configuration: PlutoGridConfiguration(
style: plutoGridStyleConfig(context),
),
);
}
Widget _buildVideoCard(MediaFileModel video, VideosProvider provider) {
return Card(
margin: const EdgeInsets.only(bottom: 16),
color: AppTheme.of(context).secondaryBackground,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(
color: AppTheme.of(context).primaryColor.withOpacity(0.2),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (video.fileUrl != null)
ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(12),
topRight: Radius.circular(12),
),
child: AspectRatio(
aspectRatio: 16 / 9,
child: Image.network(
video.fileUrl!,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) => Container(
color: AppTheme.of(context).tertiaryBackground,
child: Icon(
Icons.video_library,
size: 64,
color: AppTheme.of(context).tertiaryText,
),
),
),
),
),
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
video.title ?? video.fileName,
style: AppTheme.of(context).title3.override(
fontFamily: 'Poppins',
color: AppTheme.of(context).primaryText,
fontWeight: FontWeight.bold,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const Gap(8),
if (video.fileDescription != null &&
video.fileDescription!.isNotEmpty)
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
video.fileDescription!,
style: AppTheme.of(context).bodyText2.override(
fontFamily: 'Poppins',
color: AppTheme.of(context).secondaryText,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
Row(
children: [
Icon(
Icons.play_circle_filled,
size: 16,
color: AppTheme.of(context).tertiaryText,
),
const Gap(4),
Text(
'${video.reproducciones} reproducciones',
style: AppTheme.of(context).bodyText2.override(
fontFamily: 'Poppins',
color: AppTheme.of(context).tertiaryText,
fontSize: 12,
),
),
if (video.durationSeconds != null) ...[
const Gap(12),
Icon(
Icons.access_time,
size: 16,
color: AppTheme.of(context).tertiaryText,
),
const Gap(4),
Text(
_formatDuration(video.durationSeconds!),
style: AppTheme.of(context).bodyText2.override(
fontFamily: 'Poppins',
color: AppTheme.of(context).tertiaryText,
fontSize: 12,
),
),
],
],
),
const Gap(12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton.icon(
onPressed: () => _playVideo(video),
icon: const Icon(Icons.play_circle_outline, size: 18),
label: const Text('Reproducir'),
style: TextButton.styleFrom(
foregroundColor: const Color(0xFF4EC9F5),
),
),
TextButton.icon(
onPressed: () => _editVideo(video, provider),
icon: const Icon(Icons.edit, size: 18),
label: const Text('Editar'),
style: TextButton.styleFrom(
foregroundColor: const Color(0xFFFFB733),
),
),
TextButton.icon(
onPressed: () => _deleteVideo(video, provider),
icon: const Icon(Icons.delete, size: 18),
label: const Text('Eliminar'),
style: TextButton.styleFrom(
foregroundColor: const Color(0xFFFF2D2D),
),
),
],
),
],
),
),
],
),
);
}
Widget _buildEmptyState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.video_library_outlined,
size: 80,
color: AppTheme.of(context).tertiaryText,
),
const Gap(16),
Text(
'No hay videos disponibles',
style: AppTheme.of(context).title2.override(
fontFamily: 'Poppins',
color: AppTheme.of(context).primaryText,
fontWeight: FontWeight.bold,
),
),
const Gap(8),
Text(
'Sube tu primer video para comenzar',
style: AppTheme.of(context).bodyText1.override(
fontFamily: 'Poppins',
color: AppTheme.of(context).tertiaryText,
),
),
const Gap(24),
ElevatedButton.icon(
onPressed: () {
final provider =
Provider.of<VideosProvider>(context, listen: false);
_showUploadDialog(provider);
},
icon: const Icon(Icons.upload_file),
label: const Text('Subir Video'),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.of(context).primaryColor,
foregroundColor: const Color(0xFF0B0B0D),
padding: const EdgeInsets.symmetric(
horizontal: 32,
vertical: 16,
),
),
),
],
),
);
}
Future<void> _showUploadDialog(VideosProvider provider) async {
await showDialog(
context: context,
barrierDismissible: false,
builder: (context) => PremiumUploadDialog(
provider: provider,
onSuccess: () {
_loadData();
},
),
);
}
void _playVideo(MediaFileModel video) {
// TODO: Implementar reproductor de video
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Reproduciendo: ${video.title ?? video.fileName}'),
),
);
}
Future<void> _editVideo(MediaFileModel video, VideosProvider provider) async {
final titleController = TextEditingController(text: video.title);
final descriptionController =
TextEditingController(text: video.fileDescription);
MediaCategoryModel? selectedCategory = provider.categories
.where((cat) => cat.mediaCategoriesId == video.mediaCategoryFk)
.firstOrNull;
await showDialog(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setDialogState) => AlertDialog(
backgroundColor: AppTheme.of(context).secondaryBackground,
title: Row(
children: [
Icon(
Icons.edit,
color: AppTheme.of(context).primaryColor,
),
const Gap(12),
Expanded(
child: Text(
'Editar Video',
style: AppTheme.of(context).title2.override(
fontFamily: 'Poppins',
color: AppTheme.of(context).primaryText,
fontWeight: FontWeight.bold,
),
),
),
],
),
content: SizedBox(
width: 500,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormField(
controller: titleController,
decoration: InputDecoration(
labelText: 'Título',
filled: true,
fillColor: AppTheme.of(context).tertiaryBackground,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none,
),
),
),
const Gap(16),
TextFormField(
controller: descriptionController,
maxLines: 3,
decoration: InputDecoration(
labelText: 'Descripción',
filled: true,
fillColor: AppTheme.of(context).tertiaryBackground,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none,
),
),
),
const Gap(16),
DropdownButtonFormField<MediaCategoryModel>(
value: selectedCategory,
decoration: InputDecoration(
labelText: 'Categoría',
filled: true,
fillColor: AppTheme.of(context).tertiaryBackground,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none,
),
),
items: provider.categories.map((category) {
return DropdownMenuItem(
value: category,
child: Text(category.categoryName),
);
}).toList(),
onChanged: (value) {
setDialogState(() => selectedCategory = value);
},
),
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
'Cancelar',
style: TextStyle(
color: AppTheme.of(context).secondaryText,
),
),
),
ElevatedButton(
onPressed: () async {
Navigator.pop(context);
// Actualizar campos
if (titleController.text != video.title) {
await provider.updateVideoTitle(
video.mediaFileId,
titleController.text,
);
}
if (descriptionController.text != video.fileDescription) {
await provider.updateVideoDescription(
video.mediaFileId,
descriptionController.text,
);
}
if (selectedCategory != null &&
selectedCategory!.mediaCategoriesId !=
video.mediaCategoryFk) {
await provider.updateVideoCategory(
video.mediaFileId,
selectedCategory!.mediaCategoriesId,
);
}
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Video actualizado exitosamente'),
backgroundColor: Colors.green,
),
);
await _loadData();
},
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.of(context).primaryColor,
foregroundColor: const Color(0xFF0B0B0D),
),
child: const Text('Guardar'),
),
],
),
),
);
}
Future<void> _deleteVideo(
MediaFileModel video, VideosProvider provider) async {
final confirm = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
backgroundColor: AppTheme.of(context).secondaryBackground,
title: Row(
children: [
const Icon(
Icons.warning,
color: Color(0xFFFF2D2D),
),
const Gap(12),
Text(
'Confirmar Eliminación',
style: AppTheme.of(context).title2.override(
fontFamily: 'Poppins',
color: AppTheme.of(context).primaryText,
fontWeight: FontWeight.bold,
),
),
],
),
content: Text(
'¿Estás seguro de que deseas eliminar "${video.title ?? video.fileName}"? Esta acción no se puede deshacer.',
style: AppTheme.of(context).bodyText1.override(
fontFamily: 'Poppins',
color: AppTheme.of(context).secondaryText,
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: Text(
'Cancelar',
style: TextStyle(
color: AppTheme.of(context).secondaryText,
),
),
),
ElevatedButton(
onPressed: () => Navigator.pop(context, true),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFFF2D2D),
foregroundColor: Colors.white,
),
child: const Text('Eliminar'),
),
],
),
);
if (confirm == true) {
final success = await provider.deleteVideo(video.mediaFileId);
if (!mounted) return;
if (success) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Video eliminado exitosamente'),
backgroundColor: Colors.green,
),
);
await _loadData();
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Error al eliminar el video'),
backgroundColor: Color(0xFFFF2D2D),
),
);
}
}
}
String _formatDuration(int seconds) {
final duration = Duration(seconds: seconds);
final hours = duration.inHours;
final minutes = duration.inMinutes.remainder(60);
final secs = duration.inSeconds.remainder(60);
if (hours > 0) {
return '${hours}h ${minutes}m ${secs}s';
} else if (minutes > 0) {
return '${minutes}m ${secs}s';
} else {
return '${secs}s';
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,560 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:nethive_neo/providers/visual_state_provider.dart';
import 'package:nethive_neo/pages/videos/premium_dashboard_page.dart';
import 'package:nethive_neo/pages/videos/gestor_videos_page.dart';
import 'package:nethive_neo/theme/theme.dart';
import 'package:gap/gap.dart';
class VideosLayout extends StatefulWidget {
const VideosLayout({Key? key}) : super(key: key);
@override
State<VideosLayout> createState() => _VideosLayoutState();
}
class _VideosLayoutState extends State<VideosLayout> {
int _selectedMenuIndex = 0;
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
final List<MenuItem> _menuItems = [
MenuItem(
title: 'Dashboard',
icon: Icons.dashboard,
index: 0,
),
MenuItem(
title: 'Gestor de Videos',
icon: Icons.video_library,
index: 1,
),
MenuItem(
title: 'Configuración',
icon: Icons.settings,
index: 2,
),
];
@override
Widget build(BuildContext context) {
final isMobile = MediaQuery.of(context).size.width <= 800;
return Scaffold(
key: _scaffoldKey,
backgroundColor: AppTheme.of(context).primaryBackground,
drawer: isMobile ? _buildDrawer() : null,
body: Row(
children: [
if (!isMobile) _buildSideMenu(),
Expanded(
child: Column(
children: [
_buildHeader(isMobile),
Expanded(child: _buildContent()),
],
),
),
],
),
);
}
Widget _buildHeader(bool isMobile) {
return Container(
padding: EdgeInsets.all(isMobile ? 16 : 24),
decoration: BoxDecoration(
color: AppTheme.of(context).secondaryBackground,
border: Border(
bottom: BorderSide(
color: AppTheme.of(context).primaryColor.withOpacity(0.1),
width: 1,
),
),
),
child: Row(
children: [
if (isMobile)
IconButton(
icon: const Icon(Icons.menu),
color: AppTheme.of(context).primaryText,
onPressed: () => _scaffoldKey.currentState?.openDrawer(),
),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
gradient: AppTheme.of(context).primaryGradient,
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.energy_savings_leaf,
color: Color(0xFF0B0B0D),
size: 24,
),
),
const Gap(12),
Text(
'EnergyMedia',
style: AppTheme.of(context).title2.override(
fontFamily: 'Poppins',
color: AppTheme.of(context).primaryText,
fontWeight: FontWeight.bold,
),
),
const Spacer(),
Text(
_menuItems[_selectedMenuIndex].title,
style: AppTheme.of(context).bodyText1.override(
fontFamily: 'Poppins',
color: AppTheme.of(context).secondaryText,
),
),
],
),
);
}
Widget _buildSideMenu() {
return Container(
width: 280,
decoration: BoxDecoration(
color: AppTheme.of(context).secondaryBackground,
border: Border(
right: BorderSide(
color: AppTheme.of(context).primaryColor.withOpacity(0.1),
width: 1,
),
),
),
child: Column(
children: [
// Header con gradiente premium
Container(
width: double.infinity,
padding: const EdgeInsets.all(32),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
const Color(0xFF4EC9F5),
const Color(0xFFFFB733),
],
),
boxShadow: [
BoxShadow(
color: AppTheme.of(context).primaryColor.withOpacity(0.3),
blurRadius: 20,
offset: const Offset(0, 5),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.energy_savings_leaf,
color: Color(0xFF0B0B0D),
size: 32,
),
),
const Gap(16),
Text(
'EnergyMedia',
style: AppTheme.of(context).title2.override(
fontFamily: 'Poppins',
color: const Color(0xFF0B0B0D),
fontWeight: FontWeight.bold,
fontSize: 24,
),
),
const Gap(4),
Text(
'Content Manager',
style: AppTheme.of(context).bodyText2.override(
fontFamily: 'Poppins',
color: const Color(0xFF0B0B0D).withOpacity(0.8),
fontSize: 13,
),
),
],
),
),
// Menu Items
Expanded(
child: ListView(
padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 16),
children: _menuItems.map((item) {
final isSelected = _selectedMenuIndex == item.index;
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: _buildPremiumMenuItem(
icon: item.icon,
title: item.title,
isSelected: isSelected,
onTap: () {
setState(() => _selectedMenuIndex = item.index);
},
),
);
}).toList(),
),
),
// Theme Toggle en la parte inferior
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border(
top: BorderSide(
color: AppTheme.of(context).primaryColor.withOpacity(0.1),
width: 1,
),
),
),
child: Consumer<VisualStateProvider>(
builder: (context, visualProvider, _) {
return _buildThemeToggle(visualProvider);
},
),
),
],
),
);
}
Widget _buildPremiumMenuItem({
required IconData icon,
required String title,
required bool isSelected,
required VoidCallback onTap,
}) {
return MouseRegion(
cursor: SystemMouseCursors.click,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
decoration: BoxDecoration(
gradient: isSelected
? LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
const Color(0xFF4EC9F5),
const Color(0xFFFFB733),
],
)
: null,
borderRadius: BorderRadius.circular(12),
boxShadow: isSelected
? [
BoxShadow(
color: AppTheme.of(context).primaryColor.withOpacity(0.3),
blurRadius: 12,
offset: const Offset(0, 4),
),
]
: null,
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
child: Row(
children: [
Icon(
icon,
color: isSelected
? const Color(0xFF0B0B0D)
: AppTheme.of(context).secondaryText,
size: 24,
),
const Gap(16),
Expanded(
child: Text(
title,
style: AppTheme.of(context).bodyText1.override(
fontFamily: 'Poppins',
color: isSelected
? const Color(0xFF0B0B0D)
: AppTheme.of(context).primaryText,
fontWeight:
isSelected ? FontWeight.bold : FontWeight.w500,
),
),
),
if (isSelected)
Container(
width: 6,
height: 6,
decoration: BoxDecoration(
color: const Color(0xFF0B0B0D),
shape: BoxShape.circle,
),
),
],
),
),
),
),
),
);
}
Widget _buildThemeToggle(VisualStateProvider visualProvider) {
final isDark = AppTheme.themeMode == ThemeMode.dark;
return Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: AppTheme.of(context).tertiaryBackground,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Expanded(
child: _buildThemeButton(
icon: Icons.light_mode,
label: 'Claro',
isSelected: !isDark,
onTap: () {
visualProvider.changeThemeMode(ThemeMode.light, context);
},
),
),
const Gap(4),
Expanded(
child: _buildThemeButton(
icon: Icons.dark_mode,
label: 'Oscuro',
isSelected: isDark,
onTap: () {
visualProvider.changeThemeMode(ThemeMode.dark, context);
},
),
),
],
),
);
}
Widget _buildThemeButton({
required IconData icon,
required String label,
required bool isSelected,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
gradient: isSelected
? LinearGradient(
colors: [
const Color(0xFF4EC9F5),
const Color(0xFFFFB733),
],
)
: null,
borderRadius: BorderRadius.circular(8),
boxShadow: isSelected
? [
BoxShadow(
color: AppTheme.of(context).primaryColor.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 2),
),
]
: null,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
color: isSelected
? const Color(0xFF0B0B0D)
: AppTheme.of(context).secondaryText,
size: 20,
),
const Gap(4),
Text(
label,
style: TextStyle(
color: isSelected
? const Color(0xFF0B0B0D)
: AppTheme.of(context).secondaryText,
fontSize: 11,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
fontFamily: 'Poppins',
),
),
],
),
),
);
}
Widget _buildDrawer() {
return Drawer(
backgroundColor: AppTheme.of(context).secondaryBackground,
child: Column(
children: [
Container(
width: double.infinity,
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
const Color(0xFF4EC9F5),
const Color(0xFFFFB733),
],
),
),
child: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.energy_savings_leaf,
color: Color(0xFF0B0B0D),
size: 32,
),
),
const Gap(12),
Text(
'EnergyMedia',
style: AppTheme.of(context).title2.override(
fontFamily: 'Poppins',
color: const Color(0xFF0B0B0D),
fontWeight: FontWeight.bold,
),
),
const Gap(4),
Text(
'Content Manager',
style: AppTheme.of(context).bodyText2.override(
fontFamily: 'Poppins',
color: const Color(0xFF0B0B0D).withOpacity(0.8),
),
),
],
),
),
),
Expanded(
child: ListView(
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12),
children: _menuItems.map((item) {
final isSelected = _selectedMenuIndex == item.index;
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: _buildPremiumMenuItem(
icon: item.icon,
title: item.title,
isSelected: isSelected,
onTap: () {
setState(() => _selectedMenuIndex = item.index);
Navigator.pop(context);
},
),
);
}).toList(),
),
),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border(
top: BorderSide(
color: AppTheme.of(context).primaryColor.withOpacity(0.1),
width: 1,
),
),
),
child: Consumer<VisualStateProvider>(
builder: (context, visualProvider, _) {
return _buildThemeToggle(visualProvider);
},
),
),
],
),
);
}
Widget _buildContent() {
switch (_selectedMenuIndex) {
case 0:
return const PremiumDashboardPage();
case 1:
return const GestorVideosPage();
case 2:
return _buildWorkInProgress();
default:
return const PremiumDashboardPage();
}
}
Widget _buildWorkInProgress() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.construction,
size: 64,
color: AppTheme.of(context).tertiaryText,
),
const Gap(16),
Text(
'Trabajo en Progreso',
style: AppTheme.of(context).title2.override(
fontFamily: 'Poppins',
color: AppTheme.of(context).primaryText,
fontWeight: FontWeight.bold,
),
),
const Gap(8),
Text(
'Esta sección estará disponible próximamente',
style: AppTheme.of(context).bodyText1.override(
fontFamily: 'Poppins',
color: AppTheme.of(context).tertiaryText,
),
),
],
),
);
}
}
class MenuItem {
final String title;
final IconData icon;
final int index;
MenuItem({
required this.title,
required this.icon,
required this.index,
});
}

View File

@@ -0,0 +1,634 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
import 'package:nethive_neo/models/media/media_models.dart';
import 'package:nethive_neo/providers/videos_provider.dart';
import 'package:nethive_neo/theme/theme.dart';
import 'package:nethive_neo/widgets/premium_button.dart';
import 'package:gap/gap.dart';
class PremiumUploadDialog extends StatefulWidget {
final VideosProvider provider;
final VoidCallback onSuccess;
const PremiumUploadDialog({
Key? key,
required this.provider,
required this.onSuccess,
}) : super(key: key);
@override
State<PremiumUploadDialog> createState() => _PremiumUploadDialogState();
}
class _PremiumUploadDialogState extends State<PremiumUploadDialog> {
final titleController = TextEditingController();
final descriptionController = TextEditingController();
MediaCategoryModel? selectedCategory;
Uint8List? selectedVideo;
String? videoFileName;
Uint8List? selectedPoster;
String? posterFileName;
VideoPlayerController? _videoController;
bool isUploading = false;
@override
void dispose() {
titleController.dispose();
descriptionController.dispose();
_videoController?.dispose();
super.dispose();
}
Future<void> _selectVideo() async {
final result = await widget.provider.selectVideo();
if (result) {
setState(() {
selectedVideo = widget.provider.webVideoBytes;
videoFileName = widget.provider.videoName;
titleController.text = widget.provider.tituloController.text;
});
// Crear video player para preview (solo web)
// Para preview en web, necesitaríamos crear un Blob URL, pero esto es complejo
// Por ahora mostraremos solo el nombre y poster
}
}
Future<void> _selectPoster() async {
final result = await widget.provider.selectPoster();
if (result) {
setState(() {
selectedPoster = widget.provider.webPosterBytes;
posterFileName = widget.provider.posterName;
});
}
}
Future<void> _uploadVideo() async {
if (titleController.text.isEmpty ||
selectedCategory == null ||
selectedVideo == null ||
videoFileName == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('Por favor completa los campos requeridos'),
backgroundColor: const Color(0xFFFF2D2D),
behavior: SnackBarBehavior.floating,
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
);
return;
}
setState(() => isUploading = true);
final success = await widget.provider.uploadVideo(
title: titleController.text,
description: descriptionController.text.isEmpty
? null
: descriptionController.text,
categoryId: selectedCategory!.mediaCategoriesId,
);
if (!mounted) return;
setState(() => isUploading = false);
Navigator.pop(context);
if (success) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Row(
children: [
Icon(Icons.check_circle, color: Colors.white),
Gap(12),
Text('Video subido exitosamente'),
],
),
backgroundColor: Colors.green,
behavior: SnackBarBehavior.floating,
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
);
widget.onSuccess();
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Row(
children: [
Icon(Icons.error, color: Colors.white),
Gap(12),
Text('Error al subir el video'),
],
),
backgroundColor: const Color(0xFFFF2D2D),
behavior: SnackBarBehavior.floating,
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
);
}
}
@override
Widget build(BuildContext context) {
final isMobile = MediaQuery.of(context).size.width <= 800;
return Dialog(
backgroundColor: Colors.transparent,
child: Container(
width: isMobile ? double.infinity : 900,
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.9,
),
decoration: BoxDecoration(
color: AppTheme.of(context).secondaryBackground,
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: AppTheme.of(context).primaryColor.withOpacity(0.2),
blurRadius: 40,
offset: const Offset(0, 10),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildHeader(),
Flexible(
child: SingleChildScrollView(
padding: const EdgeInsets.all(32),
child:
isMobile ? _buildMobileContent() : _buildDesktopContent(),
),
),
_buildActions(),
],
),
),
);
}
Widget _buildHeader() {
return Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
const Color(0xFF4EC9F5),
const Color(0xFFFFB733),
],
),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(24),
topRight: Radius.circular(24),
),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.cloud_upload,
color: Color(0xFF0B0B0D),
size: 28,
),
),
const Gap(16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Subir Nuevo Video',
style: AppTheme.of(context).title2.override(
fontFamily: 'Poppins',
color: const Color(0xFF0B0B0D),
fontWeight: FontWeight.bold,
fontSize: 22,
),
),
const Gap(4),
Text(
'Comparte tu contenido con el mundo',
style: AppTheme.of(context).bodyText2.override(
fontFamily: 'Poppins',
color: const Color(0xFF0B0B0D).withOpacity(0.7),
fontSize: 13,
),
),
],
),
),
IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.close, color: Color(0xFF0B0B0D)),
tooltip: 'Cerrar',
),
],
),
);
}
Widget _buildDesktopContent() {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(flex: 3, child: _buildFormFields()),
const Gap(24),
Expanded(flex: 2, child: _buildPreviewSection()),
],
);
}
Widget _buildMobileContent() {
return Column(
children: [
_buildFormFields(),
const Gap(24),
_buildPreviewSection(),
],
);
}
Widget _buildFormFields() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildLabel('Título del Video *'),
const Gap(8),
_buildTextField(
controller: titleController,
hintText: 'Ej: Tutorial de energía solar',
prefixIcon: Icons.title,
),
const Gap(20),
_buildLabel('Descripción'),
const Gap(8),
_buildTextField(
controller: descriptionController,
hintText: 'Describe el contenido del video...',
prefixIcon: Icons.description,
maxLines: 4,
),
const Gap(20),
_buildLabel('Categoría *'),
const Gap(8),
_buildCategoryDropdown(),
],
);
}
Widget _buildPreviewSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildLabel('Vista Previa'),
const Gap(12),
_buildVideoSelector(),
const Gap(16),
_buildPosterSelector(),
],
);
}
Widget _buildLabel(String text) {
return Text(
text,
style: AppTheme.of(context).bodyText1.override(
fontFamily: 'Poppins',
color: AppTheme.of(context).primaryText,
fontWeight: FontWeight.w600,
fontSize: 14,
),
);
}
Widget _buildTextField({
required TextEditingController controller,
required String hintText,
required IconData prefixIcon,
int maxLines = 1,
}) {
return Container(
decoration: BoxDecoration(
color: AppTheme.of(context).tertiaryBackground,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppTheme.of(context).primaryColor.withOpacity(0.1),
),
),
child: TextField(
controller: controller,
maxLines: maxLines,
style: AppTheme.of(context).bodyText1.override(
fontFamily: 'Poppins',
color: AppTheme.of(context).primaryText,
),
decoration: InputDecoration(
hintText: hintText,
hintStyle: AppTheme.of(context).bodyText1.override(
fontFamily: 'Poppins',
color: AppTheme.of(context).tertiaryText,
),
prefixIcon: Icon(
prefixIcon,
color: AppTheme.of(context).primaryColor,
),
border: InputBorder.none,
contentPadding: const EdgeInsets.all(16),
),
),
);
}
Widget _buildCategoryDropdown() {
return Container(
decoration: BoxDecoration(
color: AppTheme.of(context).tertiaryBackground,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppTheme.of(context).primaryColor.withOpacity(0.1),
),
),
child: DropdownButtonFormField<MediaCategoryModel>(
value: selectedCategory,
decoration: InputDecoration(
prefixIcon: Icon(
Icons.category,
color: AppTheme.of(context).primaryColor,
),
border: InputBorder.none,
contentPadding: const EdgeInsets.all(16),
),
hint: Text(
'Selecciona una categoría',
style: AppTheme.of(context).bodyText1.override(
fontFamily: 'Poppins',
color: AppTheme.of(context).tertiaryText,
),
),
dropdownColor: AppTheme.of(context).secondaryBackground,
items: widget.provider.categories.map((category) {
return DropdownMenuItem(
value: category,
child: Text(
category.categoryName,
style: AppTheme.of(context).bodyText1.override(
fontFamily: 'Poppins',
color: AppTheme.of(context).primaryText,
),
),
);
}).toList(),
onChanged: (value) {
setState(() => selectedCategory = value);
},
),
);
}
Widget _buildVideoSelector() {
return GestureDetector(
onTap: _selectVideo,
child: Container(
height: 200,
decoration: BoxDecoration(
color: AppTheme.of(context).tertiaryBackground,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: videoFileName != null
? Colors.green.withOpacity(0.5)
: AppTheme.of(context).primaryColor.withOpacity(0.3),
width: 2,
strokeAlign: BorderSide.strokeAlignInside,
),
),
child: selectedVideo != null
? _buildVideoPreview()
: _buildUploadPlaceholder(
icon: Icons.video_file,
title: 'Seleccionar Video',
subtitle: 'Click para elegir archivo',
),
),
);
}
Widget _buildPosterSelector() {
return GestureDetector(
onTap: _selectPoster,
child: Container(
height: 150,
decoration: BoxDecoration(
color: AppTheme.of(context).tertiaryBackground,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: posterFileName != null
? Colors.green.withOpacity(0.5)
: AppTheme.of(context).primaryColor.withOpacity(0.3),
width: 2,
),
),
child: selectedPoster != null
? _buildPosterPreview()
: _buildUploadPlaceholder(
icon: Icons.image,
title: 'Miniatura (Opcional)',
subtitle: 'Click para elegir imagen',
),
),
);
}
Widget _buildVideoPreview() {
return Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(14),
child: Container(
color: Colors.black,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.play_circle_outline,
size: 64,
color: Colors.white.withOpacity(0.8),
),
const Gap(12),
Text(
videoFileName ?? 'Video seleccionado',
style: const TextStyle(
color: Colors.white,
fontFamily: 'Poppins',
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
],
),
),
),
),
Positioned(
top: 12,
right: 12,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.circular(20),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.check_circle, size: 16, color: Colors.white),
Gap(4),
Text(
'Cargado',
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontFamily: 'Poppins',
fontWeight: FontWeight.w600,
),
),
],
),
),
),
],
);
}
Widget _buildPosterPreview() {
return Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(14),
child: Image.memory(
selectedPoster!,
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
),
),
Positioned(
top: 12,
right: 12,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.circular(20),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.check_circle, size: 16, color: Colors.white),
Gap(4),
Text(
'Cargado',
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontFamily: 'Poppins',
fontWeight: FontWeight.w600,
),
),
],
),
),
),
],
);
}
Widget _buildUploadPlaceholder({
required IconData icon,
required String title,
required String subtitle,
}) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppTheme.of(context).primaryColor.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
icon,
size: 40,
color: AppTheme.of(context).primaryColor,
),
),
const Gap(12),
Text(
title,
style: AppTheme.of(context).bodyText1.override(
fontFamily: 'Poppins',
color: AppTheme.of(context).primaryText,
fontWeight: FontWeight.w600,
),
),
const Gap(4),
Text(
subtitle,
style: AppTheme.of(context).bodyText2.override(
fontFamily: 'Poppins',
color: AppTheme.of(context).tertiaryText,
fontSize: 12,
),
),
],
),
);
}
Widget _buildActions() {
return Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: AppTheme.of(context).tertiaryBackground.withOpacity(0.5),
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(24),
bottomRight: Radius.circular(24),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
PremiumButton(
text: 'Cancelar',
isOutlined: true,
onPressed: () => Navigator.pop(context),
width: 120,
),
const Gap(12),
PremiumButton(
text: 'Subir Video',
icon: Icons.cloud_upload,
onPressed: _uploadVideo,
isLoading: isUploading,
width: 160,
),
],
),
);
}
}