save before clean
This commit is contained in:
@@ -5,6 +5,8 @@ import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:nethive_neo/theme/theme.dart';
|
||||
import 'package:nethive_neo/providers/nethive/componentes_provider.dart';
|
||||
import 'package:nethive_neo/models/nethive/topologia_completa_model.dart';
|
||||
import 'package:nethive_neo/pages/infrastructure/pages/widgets/topologia_page_widgets/rack_view_widget.dart';
|
||||
import 'package:nethive_neo/pages/infrastructure/pages/widgets/topologia_page_widgets/floor_plan_view_widget.dart';
|
||||
|
||||
class TopologiaPage extends StatefulWidget {
|
||||
const TopologiaPage({Key? key}) : super(key: key);
|
||||
@@ -1195,69 +1197,17 @@ class _TopologiaPageState extends State<TopologiaPage>
|
||||
}
|
||||
|
||||
Widget _buildRackView(bool isMediumScreen, ComponentesProvider provider) {
|
||||
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.7),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Text(
|
||||
'Vista de Racks',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Próximamente: Visualización detallada de racks\ncon ${provider.componentesTopologia.where((c) => c.esRack).length} racks detectados',
|
||||
textAlign: TextAlign.center,
|
||||
style:
|
||||
TextStyle(color: Colors.white.withOpacity(0.7), fontSize: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
return RackViewWidget(
|
||||
isMediumScreen: isMediumScreen,
|
||||
provider: provider,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFloorPlanView(
|
||||
bool isMediumScreen, ComponentesProvider provider) {
|
||||
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),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Text(
|
||||
'Plano de Planta',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Próximamente: Plano de distribución física\ncon ubicaciones de ${provider.componentesTopologia.length} componentes',
|
||||
textAlign: TextAlign.center,
|
||||
style:
|
||||
TextStyle(color: Colors.white.withOpacity(0.7), fontSize: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
return FloorPlanViewWidget(
|
||||
isMediumScreen: isMediumScreen,
|
||||
provider: provider,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,863 @@
|
||||
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}');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user