base creada
This commit is contained in:
@@ -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
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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 ? 'Sí' : '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
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
536
lib/pages/videos/dashboard_page.dart
Normal file
536
lib/pages/videos/dashboard_page.dart
Normal 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';
|
||||
}
|
||||
}
|
||||
}
|
||||
823
lib/pages/videos/gestor_videos_page.dart
Normal file
823
lib/pages/videos/gestor_videos_page.dart
Normal 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';
|
||||
}
|
||||
}
|
||||
}
|
||||
1109
lib/pages/videos/premium_dashboard_page.dart
Normal file
1109
lib/pages/videos/premium_dashboard_page.dart
Normal file
File diff suppressed because it is too large
Load Diff
560
lib/pages/videos/videos_layout.dart
Normal file
560
lib/pages/videos/videos_layout.dart
Normal 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,
|
||||
});
|
||||
}
|
||||
634
lib/pages/videos/widgets/premium_upload_dialog.dart
Normal file
634
lib/pages/videos/widgets/premium_upload_dialog.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user