From 8bbe7a53ff7e4c6e2142416bfbff740f86d8ef2f Mon Sep 17 00:00:00 2001 From: Abraham Date: Wed, 30 Jul 2025 22:29:28 -0700 Subject: [PATCH] =?UTF-8?q?referencias=20a=C3=B1adidas=20reparada=20forma?= =?UTF-8?q?=20de=20topologia?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/referencia/all_video_table.txt | 965 --------- assets/referencia/categoria_componente.png | Bin 0 -> 7846 bytes .../referencia/fn_topologia_por_negocio.txt | 92 + .../referencia/nethive_tablas_actualizado.md | 294 --- assets/referencia/rol_logico_componente.png | Bin 0 -> 34823 bytes assets/referencia/tablas_nethive.txt | 39 +- assets/referencia/tipo_distribucion.png | Bin 0 -> 12543 bytes assets/referencia/videos_provider.txt | 1249 ------------ lib/models/nethive/componente_model.dart | 10 +- .../nethive/conexion_alimentacion_model.dart | 39 + .../nethive/rol_logico_componente_model.dart | 58 + .../nethive/tipo_distribucion_model.dart | 51 + .../nethive/topologia_completa_model.dart | 199 ++ .../vista_topologia_por_negocio_model.dart | 112 +- .../infrastructure/pages/topologia_page.dart | 1773 ++++++++--------- .../nethive/componentes_provider.dart | 1266 ++++++------ 16 files changed, 1959 insertions(+), 4188 deletions(-) delete mode 100644 assets/referencia/all_video_table.txt create mode 100644 assets/referencia/categoria_componente.png create mode 100644 assets/referencia/fn_topologia_por_negocio.txt delete mode 100644 assets/referencia/nethive_tablas_actualizado.md create mode 100644 assets/referencia/rol_logico_componente.png create mode 100644 assets/referencia/tipo_distribucion.png delete mode 100644 assets/referencia/videos_provider.txt create mode 100644 lib/models/nethive/conexion_alimentacion_model.dart create mode 100644 lib/models/nethive/rol_logico_componente_model.dart create mode 100644 lib/models/nethive/tipo_distribucion_model.dart create mode 100644 lib/models/nethive/topologia_completa_model.dart diff --git a/assets/referencia/all_video_table.txt b/assets/referencia/all_video_table.txt deleted file mode 100644 index 602cf68..0000000 --- a/assets/referencia/all_video_table.txt +++ /dev/null @@ -1,965 +0,0 @@ -import 'package:cbluna_crm_lu/helpers/globals.dart'; -import 'package:cbluna_crm_lu/pages/content_manager/widget/edit_video_popup_neo.dart'; -import 'package:cbluna_crm_lu/pages/widgets/pluto_grid/pluto_grid_header.dart'; -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; -import 'package:pluto_grid/pluto_grid.dart'; -import 'package:cbluna_crm_lu/pages/content_manager/widget/edit_video_popup.dart'; -import 'package:cbluna_crm_lu/pages/content_manager/widget/popup_detalle_video.dart'; -import 'package:cbluna_crm_lu/pages/content_manager/widget/popup_eliminar_video.dart'; -import 'package:cbluna_crm_lu/pages/widgets/animated_hover_button.dart'; - -import '../../../providers/videos_provider.dart'; -import '../../../theme/theme.dart'; - -import 'package:url_launcher/url_launcher.dart'; -import 'package:toggle_switch/toggle_switch.dart'; - -class AllVideoTable extends StatelessWidget { - const AllVideoTable({ - Key? key, - required this.providerAd, - }) : super(key: key); - - final VideosProvider providerAd; - - @override - Widget build(BuildContext context) { - return Flexible( - child: PlutoGrid( - key: UniqueKey(), - configuration: PlutoGridConfiguration( - enableMoveDownAfterSelecting: true, - enableMoveHorizontalInEditing: true, - localeText: const PlutoGridLocaleText.spanish(), - scrollbar: plutoGridScrollbarConfig(context), - style: plutoGridStyleConfigContentManager(context, rowHeight: 150), - columnFilter: const PlutoGridColumnFilterConfig( - filters: [ - ...FilterHelper.defaultFilters, - ], - ), - ), - columns: [ - PlutoColumn( - titleSpan: plutoGridHeader( - context: context, - icono: Icons.numbers, - color: AppTheme.of(context).primaryBackground, - texto: 'ID', - ), - title: 'ID', - field: 'video_id', - titleTextAlign: PlutoColumnTextAlign.center, - textAlign: PlutoColumnTextAlign.center, - width: 100, - type: PlutoColumnType.text(), - cellPadding: const EdgeInsets.all(0), - enableEditingMode: false, - backgroundColor: AppTheme.of(context).primaryColor, - enableContextMenu: false, - enableDropToResize: false, - renderer: (rendererContext) { - return rendererContext.cell.row.cells['categories']!.value - .toString() - .replaceAll(RegExp(r'^\[|\]$'), '') - .replaceAll('", "', ', ') - .replaceAll('null', '') - .isEmpty - ? Row( - mainAxisSize: MainAxisSize.max, - children: [ - Container( - color: Colors.yellowAccent[400], - child: Center( - child: //icono warning - Icon( - Icons.warning, - color: AppTheme.of(context).tertiaryText, - size: 24, - ), - ), - ), - Expanded( - child: Container( - color: Colors.yellowAccent[400], - child: Center( - child: Text( - rendererContext.cell.value.toString(), - textAlign: TextAlign.center, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 18, - fontWeight: FontWeight.w500, - ), - ), - ), - ), - ) - ], - ) - : Text( - rendererContext.cell.value.toString(), - textAlign: TextAlign.center, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 18, - fontWeight: FontWeight.w500, - ), - ); - }, - ), - PlutoColumn( - titleSpan: plutoGridHeader( - context: context, - icono: Icons.play_arrow, - color: AppTheme.of(context).primaryBackground, - texto: 'Video', - ), - title: 'Video', - field: 'video_url', - width: 225, - titleTextAlign: PlutoColumnTextAlign.center, - textAlign: PlutoColumnTextAlign.center, - type: PlutoColumnType.text(), - enableEditingMode: false, - backgroundColor: AppTheme.of(context).primaryColor, - enableContextMenu: false, - enableDropToResize: false, - renderer: (rendererContext) { - try { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - height: 250, - width: 100, - clipBehavior: Clip.antiAlias, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20)), - child: Image.network( - rendererContext.row.cells['poster_path']!.value, - fit: BoxFit.cover, - )), - const SizedBox(width: 10), - Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - color: AppTheme.of(context).primaryBackground, - ), - child: AnimatedHoverButton( - icon: Icons.play_arrow, - tooltip: 'Iniciar', - size: 48, - primaryColor: AppTheme.of(context).primaryBackground, - secondaryColor: AppTheme.of(context).secondaryColor, - onTap: () async { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - backgroundColor: Colors.transparent, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - content: PopupDetallesVideo( - tituloEncabezado: "Video actual", - url: rendererContext.cell.value.toString(), - titulo: - rendererContext.row.cells['title']!.value, - video: rendererContext.row.cells, - ), - // Widget personalizado - ); - }, - ); - }, - ), - ), - ], - ); - } catch (e) { - return Container( - color: Colors.transparent, - child: const Text("--", textAlign: TextAlign.center)); - } - }, - ), - PlutoColumn( - titleSpan: plutoGridHeader( - context: context, - icono: Icons.calendar_month, - color: AppTheme.of(context).primaryBackground, - texto: 'Fecha de creación', - ), - title: 'Created at', - field: 'created_at', - titleTextAlign: PlutoColumnTextAlign.center, - textAlign: PlutoColumnTextAlign.center, - width: 150, - type: PlutoColumnType.date(), - enableEditingMode: false, - enableAutoEditing: false, - backgroundColor: AppTheme.of(context).primaryColor, - enableContextMenu: false, - enableDropToResize: false, - renderer: (rendererContext) { - // Obtiene el valor de la fecha - String? fecha = rendererContext.row.cells['created_at']?.value; - - // Uso de operador ternario para verificar si hay fecha y formatearla - String fechaFormateada = (fecha != null && fecha.isNotEmpty) - ? DateFormat("dd 'de' MMMM 'de' yyyy", 'es_ES') - .format(DateTime.parse(fecha)) - : '--'; - - return SizedBox( - width: 250, - child: Text( - fechaFormateada, - textAlign: TextAlign.center, - maxLines: 3, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 16, - fontWeight: FontWeight.w500, - ), - ), - ); - }, - ), - PlutoColumn( - titleSpan: plutoGridHeader( - context: context, - icono: Icons.check_circle, - color: AppTheme.of(context).primaryBackground, - texto: 'Estado', - ), - title: 'Estado', - field: 'video_status', - titleTextAlign: PlutoColumnTextAlign.center, - textAlign: PlutoColumnTextAlign.center, - width: 200, - type: PlutoColumnType.text(), - enableEditingMode: false, - backgroundColor: AppTheme.of(context).primaryColor, - enableContextMenu: false, - enableDropToResize: false, - renderer: (rendererContext) { - //usa el tooltip para mostrar el estado del video - return MouseRegion( - cursor: SystemMouseCursors.click, - child: ToggleSwitch( - centerText: true, - minWidth: 90.0, - cornerRadius: 20.0, - fontSize: 14, - activeBgColor: [AppTheme.of(context).primaryColor], - activeFgColor: Colors.white, - inactiveBgColor: - AppTheme.of(context).primaryText.withOpacity(0.7), - inactiveFgColor: Colors.white, - activeBgColors: [ - [AppTheme.of(context).primaryColor], - const [Colors.red] - ], - borderWidth: 2.0, - borderColor: [ - AppTheme.of(context).tertiaryText.withOpacity(0.7) - ], - labels: const ['Activo', 'Inactivo'], - initialLabelIndex: - rendererContext.cell.value == true ? 0 : 1, - onToggle: (index) { - rendererContext.cell.row.cells['video_status']!.value = - index == 0 - ? true - : false; //cambia el valor en la celda - providerAd.cambiarEstadoVideo( - rendererContext.cell.row.cells['video_id']!.value, - index == 0 ? true : false); - }, - ), - ); - }), - PlutoColumn( - titleSpan: plutoGridHeader( - context: context, - icono: Icons.star, - color: AppTheme.of(context).primaryBackground, - texto: 'Prioridad', - ), - title: 'Priority', - field: 'priority', - titleTextAlign: PlutoColumnTextAlign.center, - textAlign: PlutoColumnTextAlign.center, - width: 125, - type: PlutoColumnType.number(), - enableEditingMode: false, - backgroundColor: AppTheme.of(context).primaryColor, - enableContextMenu: false, - enableDropToResize: false, - renderer: (rendererContext) { - return Container( - width: 100, - padding: const EdgeInsets.all(5), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: rendererContext.row.cells['priority']!.value == 4 - ? const Color(0xFFD90D56).withOpacity(0.5) - : rendererContext.row.cells['priority']!.value == 3 - ? const Color(0xFFFFC700).withOpacity(0.5) - : rendererContext.row.cells['priority']!.value == - 2 - ? const Color(0xFF517EF2).withOpacity(0.5) - : const Color(0x5A0E2152)), - child: Text( - rendererContext.row.cells['priority']!.value == 4 - ? 'alta' - : rendererContext.row.cells['priority']!.value == 3 - ? 'media' - : rendererContext.row.cells['priority']!.value == 2 - ? 'baja' - : 'neutra', - textAlign: TextAlign.center, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 18, - fontWeight: FontWeight.w500, - ), - )); - }, - ), - PlutoColumn( - titleSpan: plutoGridHeader( - context: context, - icono: Icons.title, - color: AppTheme.of(context).primaryBackground, - texto: 'Titulo', - ), - title: 'Title', - field: 'title', - titleTextAlign: PlutoColumnTextAlign.center, - textAlign: PlutoColumnTextAlign.center, - width: 325, - type: PlutoColumnType.text(), - enableEditingMode: false, - enableAutoEditing: false, - backgroundColor: AppTheme.of(context).primaryColor, - enableContextMenu: false, - enableDropToResize: false, - renderer: (rendererContext) { - return SizedBox( - width: 150, - child: Text( - rendererContext.row.cells['title']!.value ?? '--', - textAlign: TextAlign.center, - maxLines: 3, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 18, - fontWeight: FontWeight.w500, - ), - ), - ); - }, - ), - PlutoColumn( - titleSpan: plutoGridHeader( - context: context, - icono: Icons.description, - color: AppTheme.of(context).primaryBackground, - texto: 'Descripción', - ), - title: 'Descripción', - field: 'overview', - titleTextAlign: PlutoColumnTextAlign.center, - textAlign: PlutoColumnTextAlign.center, - width: 350, - type: PlutoColumnType.text(), - enableEditingMode: false, - enableAutoEditing: false, - backgroundColor: AppTheme.of(context).primaryColor, - enableContextMenu: false, - enableDropToResize: false, - renderer: (rendererContext) { - return providerAd.showWarningsOnTable == true - ? Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: Container( - color: rendererContext.cell.value.isEmpty - ? Colors.yellowAccent[400] - : Colors.transparent, - child: Center( - child: Text( - rendererContext.cell.value.isEmpty - ? 'NO DESCRIPTION' - : rendererContext.cell.value, - textAlign: TextAlign.center, - style: TextStyle( - color: rendererContext.cell.value.isEmpty - ? AppTheme.of(context).tertiaryText - : AppTheme.of(context).primaryText, - fontSize: rendererContext.cell.value.isEmpty - ? 18 - : 14, - fontWeight: rendererContext.cell.value.isEmpty - ? FontWeight.w800 - : FontWeight.w500, - ), - ), - ), - ), - ), - ], - ) - : SizedBox( - width: 200, - child: Text( - rendererContext.row.cells['overview']!.value, - textAlign: TextAlign.center, - maxLines: 4, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 14, - fontWeight: FontWeight.w500, - ), - ), - ); - }, - ), - PlutoColumn( - titleSpan: plutoGridHeader( - context: context, - icono: Icons.qr_code_2_rounded, - color: AppTheme.of(context).primaryBackground, - texto: 'QRs generados', - ), - title: 'QRs generados', - field: 'qr_codes', - 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 qrCodes = rendererContext.row.cells['qr_codes']?.value; - - // Verifica que qrCodes no esté vacío antes de mostrar el botón - if (qrCodes != null && qrCodes.isNotEmpty) { - return Center( - child: ElevatedButton.icon( - icon: Icon(Icons.list_alt_rounded, size: 18), - label: Text( - 'Ver QRs', - style: TextStyle(fontSize: 13), - ), - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric( - horizontal: 12, vertical: 6), - backgroundColor: AppTheme.of(context).primaryColor, - foregroundColor: Colors.white, - ), - onPressed: () { - showDialog( - context: context, - builder: (_) => AlertDialog( - backgroundColor: - AppTheme.of(context).primaryBackground, - title: const Text('Lista de QRs'), - content: SizedBox( - width: 400, - height: 300, - child: ListView.builder( - itemCount: qrCodes - .split('\n') - .length, // Divide los QR Codes concatenados - itemBuilder: (context, index) { - try { - final qrData = qrCodes.split( - '\n')[index]; // Obtiene el QR individual - return Padding( - padding: - const EdgeInsets.symmetric(vertical: 4), - child: Text( - qrData, - style: const TextStyle(fontSize: 14), - ), - ); - } catch (e) { - return Text( - 'Error al leer QR: $e', - style: const TextStyle(color: Colors.red), - ); - } - }, - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Cerrar'), - ), - ], - ), - ); - }, - ), - ); - } else { - return Center( - child: Text( - 'GLOBAL', - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 18, - fontWeight: FontWeight.w900, - ), - )); - } - }, - ), - PlutoColumn( - titleSpan: plutoGridHeader( - context: context, - icono: Icons.link, - color: AppTheme.of(context).primaryBackground, - texto: 'Enlace', - ), - title: 'OutLink', - field: 'url_ad', - titleTextAlign: PlutoColumnTextAlign.center, - textAlign: PlutoColumnTextAlign.center, - width: 100, - type: PlutoColumnType.text(), - enableEditingMode: false, - backgroundColor: AppTheme.of(context).primaryColor, - enableContextMenu: false, - enableDropToResize: false, - renderer: (rendererContext) { - String? url = rendererContext.row.cells['url_ad']?.value; - bool isValidUrl = url != null && url != 'null' && url.isNotEmpty; - return providerAd.showWarningsOnTable == true - ? Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - color: isValidUrl - ? Colors.transparent - : Colors.yellowAccent[400], - child: IconButton( - onPressed: () async { - if (isValidUrl) { - String finalUrl = - url!.startsWith(RegExp(r'http://|https://')) - ? url - : 'http://$url'; - if (await canLaunchUrl(Uri.parse(finalUrl))) { - await launchUrl(Uri.parse(finalUrl)); - } else { - print('No se puede abrir el enlace'); - } - } else { - print('No se puede abrir el enlace'); - } - }, - icon: Icon( - isValidUrl ? Icons.link : Icons.link_off, - color: isValidUrl - ? AppTheme.of(context).primaryColor - : AppTheme.of(context).tertiaryText, - semanticLabel: - isValidUrl ? 'Open link' : 'No link', - ), - ), - ), - if (!isValidUrl) - const Text('No link added', - style: TextStyle( - fontSize: 16, fontWeight: FontWeight.w800)), - ], - ) - : IconButton( - onPressed: () async { - if (isValidUrl) { - // Asegurarse de que 'url' comience con 'http://' o 'https://' - String finalUrl = - url!.startsWith(RegExp(r'http://|https://')) - ? url - : 'http://$url'; - if (await canLaunchUrl(Uri.parse(finalUrl))) { - await launchUrl(Uri.parse(finalUrl)); - } else { - print('No se puede abrir el enlace'); - } - } else { - print('No se puede abrir el enlace'); - } - }, - icon: Icon( - // Usar 'isValidUrl' para determinar el ícono a mostrar. - isValidUrl ? Icons.link : Icons.link_off, - color: AppTheme.of(context).primaryColor, - ), - ); - }, - ), - PlutoColumn( - titleSpan: plutoGridHeader( - context: context, - icono: Icons.category, - color: AppTheme.of(context).primaryBackground, - texto: 'Categoría de video', - ), - title: 'Categoría', - field: 'categories', - titleTextAlign: PlutoColumnTextAlign.center, - textAlign: PlutoColumnTextAlign.center, - width: 200, - type: PlutoColumnType.text(), - enableEditingMode: false, - backgroundColor: AppTheme.of(context).primaryColor, - enableContextMenu: false, - enableDropToResize: false, - renderer: (rendererContext) { - var textoLimpio = rendererContext.cell.value - .toString() - .replaceAll(RegExp(r'^\[|\]$'), '') - .replaceAll('", "', ', ') - .replaceAll('null', ''); - - return Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: Container( - color: textoLimpio.isEmpty - ? Colors.yellowAccent[400] - : Colors.transparent, - child: Center( - child: Text( - textoLimpio.isEmpty ? 'NO CATEGORIES' : textoLimpio, - textAlign: TextAlign.center, - style: TextStyle( - color: textoLimpio.isEmpty - ? AppTheme.of(context).tertiaryText - : AppTheme.of(context).primaryText, - fontSize: textoLimpio.isEmpty ? 18 : 16, - fontWeight: textoLimpio.isEmpty - ? FontWeight.w800 - : FontWeight.w500, - ), - ), - ), - ), - ), - ], - ); - }, - ), - PlutoColumn( - titleSpan: plutoGridHeader( - context: context, - icono: Icons.person, - color: AppTheme.of(context).primaryBackground, - texto: 'Patrocinador', - ), - title: 'Partner', - field: 'partner', - titleTextAlign: PlutoColumnTextAlign.center, - textAlign: PlutoColumnTextAlign.center, - width: 150, - type: PlutoColumnType.text(), - enableEditingMode: false, - backgroundColor: AppTheme.of(context).primaryColor, - enableContextMenu: false, - enableDropToResize: false, - renderer: (rendererContext) { - return providerAd.showWarningsOnTable == true - ? Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: Container( - color: rendererContext.cell.value.isEmpty - ? Colors.yellowAccent[400] - : Colors.transparent, - child: Center( - child: Text( - rendererContext.cell.value.isEmpty - ? 'NO PARTNER' - : rendererContext.cell.value, - textAlign: TextAlign.center, - style: TextStyle( - color: rendererContext.cell.value.isEmpty - ? AppTheme.of(context).tertiaryText - : AppTheme.of(context).primaryText, - fontSize: rendererContext.cell.value.isEmpty - ? 18 - : 16, - fontWeight: rendererContext.cell.value.isEmpty - ? FontWeight.w800 - : FontWeight.w500, - ), - ), - ), - ), - ), - ], - ) - : SizedBox( - width: 200, - child: Text( - rendererContext.row.cells['partner']!.value ?? '--', - textAlign: TextAlign.center, - maxLines: 4, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 18, - fontWeight: FontWeight.w500, - ), - ), - ); - }, - ), - /* PlutoColumn( - titleSpan: plutoGridHeader( - context: context, - icono: Icons.calendar_today, - color: AppTheme.of(context).primaryBackground, - texto: 'Fecha expiración', - ), - title: 'Exp. date', - field: 'expirationDate', - titleTextAlign: PlutoColumnTextAlign.center, - textAlign: PlutoColumnTextAlign.center, - width: 180, - type: PlutoColumnType.date(), - enableEditingMode: false, - backgroundColor: AppTheme.of(context).primaryColor, - enableContextMenu: false, - enableDropToResize: false, - ), */ - PlutoColumn( - titleSpan: plutoGridHeader( - context: context, - icono: Icons.timelapse, - color: AppTheme.of(context).primaryBackground, - texto: 'Duración', - ), - title: 'Duration', - field: 'duration_video', - titleTextAlign: PlutoColumnTextAlign.center, - textAlign: PlutoColumnTextAlign.center, - width: 130, - type: PlutoColumnType.number(), - enableEditingMode: false, - backgroundColor: AppTheme.of(context).primaryColor, - enableContextMenu: false, - enableDropToResize: false, - renderer: (rendererContext) { - int durationInSeconds = rendererContext.cell.value; - String minutes = - (durationInSeconds ~/ 60).toString().padLeft(2, '0'); - String seconds = - (durationInSeconds % 60).toString().padLeft(2, '0'); - return Text( - '$minutes:$seconds', - textAlign: TextAlign.center, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 14, - fontWeight: FontWeight.w500, - ), - ); - }, - ), - PlutoColumn( - titleSpan: plutoGridHeader( - context: context, - icono: Icons.edit, - color: AppTheme.of(context).primaryBackground, - texto: 'Editar', - ), - title: 'Edit', - field: 'editar', - width: 100, - titleTextAlign: PlutoColumnTextAlign.center, - textAlign: PlutoColumnTextAlign.center, - type: PlutoColumnType.number(), - enableEditingMode: false, - backgroundColor: AppTheme.of(context).primaryColor, - enableContextMenu: false, - enableDropToResize: false, - renderer: (rendererContext) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - alignment: Alignment.center, - child: AnimatedHoverButton( - icon: Icons.edit, - tooltip: 'Edit', - primaryColor: AppTheme.of(context).primaryBackground, - secondaryColor: AppTheme.of(context).primaryColor, - onTap: () async { - providerAd.resetAllvideoData(); - final List qrsAsociados = - await providerAd.getQrsByVideoId(rendererContext - .cell.row.cells['video_id']!.value); - - providerAd.listaQrsSeleccionados = qrsAsociados; - showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext context) { - providerAd.selectedVideoId = rendererContext - .cell.row.cells['video_id']!.value; - - providerAd.tituloVideo = rendererContext - .cell.row.cells['title']!.value; - - providerAd.videoPoints = rendererContext - .cell.row.cells['points']!.value; - - providerAd.videoName = rendererContext - .cell.row.cells['title']!.value; - - providerAd.videoPatner = rendererContext - .cell.row.cells['partner']!.value; - - providerAd.descripcionVideo = rendererContext - .cell.row.cells['overview']!.value; - - providerAd.videoOutlink = rendererContext - .cell.row.cells['url_ad']!.value; - //video_poster_file - providerAd.videoCoverFile = rendererContext - .cell.row.cells['poster_file_name']!.value; - - providerAd.videoCoverFileNameNoModif = - rendererContext.cell.row - .cells['poster_file_name']!.value; - - providerAd.selectePriority = rendererContext - .row.cells['priority']!.value == - 4 - ? 'alta' - : rendererContext - .row.cells['priority']!.value == - 3 - ? 'media' - : rendererContext.row.cells['priority']! - .value == - 2 - ? 'baja' - : 'neutra'; - - providerAd.videoPath = rendererContext - .cell.row.cells['video_url']!.value; - - providerAd.videoPosterPath = rendererContext - .cell.row.cells['poster_path']!.value; - - return AlertDialog( - backgroundColor: - AppTheme.of(context).primaryBackground, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(50), - ), - content: PopupEditVideoNeo( - provider: providerAd, - editMode: true, - currentCategories: rendererContext - .cell.row.cells['categories']!.value, - )); - }, - ); - }, - ), - ), - ], - ); - }), - PlutoColumn( - titleSpan: plutoGridHeader( - context: context, - icono: Icons.delete, - color: AppTheme.of(context).primaryBackground, - texto: 'Eliminar', - ), - title: 'Delete', - field: 'eliminar', - width: 100, - titleTextAlign: PlutoColumnTextAlign.center, - textAlign: PlutoColumnTextAlign.center, - type: PlutoColumnType.number(), - enableEditingMode: false, - backgroundColor: AppTheme.of(context).primaryColor, - enableContextMenu: false, - enableDropToResize: false, - renderer: (rendererContext) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - alignment: Alignment.center, - child: AnimatedHoverButton( - icon: Icons.delete, - tooltip: 'Delete this video', - primaryColor: AppTheme.of(context).primaryBackground, - secondaryColor: Colors.redAccent, - onTap: () async { - showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext context) { - return EliminarVideoPopup( - idVideo: rendererContext - .cell.row.cells['video_id']!.value, - nombreVideo: rendererContext - .cell.row.cells['title']!.value, - videoFileName: rendererContext - .cell.row.cells['video_file_name']!.value, - posterFileName: rendererContext - .cell.row.cells['poster_file_name']!.value, - videoUrl: rendererContext - .cell.row.cells['video_url']!.value, - ); - }, - ); - }, - ), - ), - ], - ); - }), - ], - rows: providerAd.allVideoRows, - onLoaded: (event) async { - providerAd.listStateManager.add(event.stateManager); - }, - createFooter: (stateManager) { - stateManager.setPageSize(10, notify: false); // default 40 - - return PlutoPagination(stateManager); - }, - ), - ); - } -} diff --git a/assets/referencia/categoria_componente.png b/assets/referencia/categoria_componente.png new file mode 100644 index 0000000000000000000000000000000000000000..689f4d6171f3824ecc4ae8aca7a3bed26b70294f GIT binary patch literal 7846 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 literal 0 HcmV?d00001 diff --git a/assets/referencia/fn_topologia_por_negocio.txt b/assets/referencia/fn_topologia_por_negocio.txt new file mode 100644 index 0000000..2c9d263 --- /dev/null +++ b/assets/referencia/fn_topologia_por_negocio.txt @@ -0,0 +1,92 @@ +CREATE OR REPLACE FUNCTION nethive.fn_topologia_por_negocio(p_negocio_id uuid) +RETURNS jsonb +LANGUAGE plpgsql +AS $$ +BEGIN + RETURN ( + SELECT jsonb_build_object( + 'componentes', ( + SELECT jsonb_agg(row_to_json(c)) + FROM ( + SELECT + comp.id, + comp.nombre, + comp.categoria_id, + cat.nombre AS categoria, + comp.rol_logico_id, + r.nombre AS rol_logico, + comp.descripcion, + comp.ubicacion, + comp.imagen_url, + comp.en_uso, + comp.activo, + comp.fecha_registro, + comp.distribucion_id, + td.nombre AS tipo_distribucion, + d.nombre AS nombre_distribucion + FROM nethive.componente comp + LEFT JOIN nethive.categoria_componente cat ON comp.categoria_id = cat.id + LEFT JOIN nethive.rol_logico_componente r ON comp.rol_logico_id = r.id + LEFT JOIN nethive.distribucion d ON comp.distribucion_id = d.id + LEFT JOIN nethive.tipo_distribucion td ON d.tipo_id = td.id + WHERE comp.negocio_id = p_negocio_id + ) AS c + ), + + 'conexiones_datos', ( + SELECT jsonb_agg(row_to_json(cd)) + FROM ( + SELECT + cc.id, + cc.componente_origen_id, + co.nombre AS nombre_origen, + ro.id AS rol_logico_origen_id, + ro.nombre AS rol_logico_origen, + cc.componente_destino_id, + cd.nombre AS nombre_destino, + rd.id AS rol_logico_destino_id, + rd.nombre AS rol_logico_destino, + cc.cable_id, + cb.nombre AS nombre_cable, + cc.descripcion, + cc.activo + FROM nethive.conexion_componente cc + LEFT JOIN nethive.componente co ON cc.componente_origen_id = co.id + LEFT JOIN nethive.rol_logico_componente ro ON co.rol_logico_id = ro.id + LEFT JOIN nethive.componente cd ON cc.componente_destino_id = cd.id + LEFT JOIN nethive.rol_logico_componente rd ON cd.rol_logico_id = rd.id + LEFT JOIN nethive.componente cb ON cc.cable_id = cb.id + WHERE co.negocio_id = p_negocio_id OR cd.negocio_id = p_negocio_id + ) AS cd + ), + + 'conexiones_energia', ( + SELECT jsonb_agg(row_to_json(ce)) + FROM ( + SELECT + ca.id, + ca.origen_id, + co.nombre AS nombre_origen, + ro.id AS rol_logico_origen_id, + ro.nombre AS rol_logico_origen, + ca.destino_id, + cd.nombre AS nombre_destino, + rd.id AS rol_logico_destino_id, + rd.nombre AS rol_logico_destino, + ca.cable_id, + cb.nombre AS nombre_cable, + ca.descripcion, + ca.activo + FROM nethive.conexion_alimentacion ca + LEFT JOIN nethive.componente co ON ca.origen_id = co.id + LEFT JOIN nethive.rol_logico_componente ro ON co.rol_logico_id = ro.id + LEFT JOIN nethive.componente cd ON ca.destino_id = cd.id + LEFT JOIN nethive.rol_logico_componente rd ON cd.rol_logico_id = rd.id + LEFT JOIN nethive.componente cb ON ca.cable_id = cb.id + WHERE co.negocio_id = p_negocio_id OR cd.negocio_id = p_negocio_id + ) AS ce + ) + ) + ); +END; +$$; diff --git a/assets/referencia/nethive_tablas_actualizado.md b/assets/referencia/nethive_tablas_actualizado.md deleted file mode 100644 index 44af195..0000000 --- a/assets/referencia/nethive_tablas_actualizado.md +++ /dev/null @@ -1,294 +0,0 @@ - -# 📘 Estructura de Base de Datos — NETHIVE (Esquema: `nethive`) - ---- - -## 🏢 `empresa` - -| Columna | Tipo de dato | Descripción | -|-----------------|----------------|-----------------------------------| -| id | UUID | PRIMARY KEY | -| nombre | TEXT | Nombre de la empresa | -| rfc | TEXT | Registro fiscal | -| direccion | TEXT | Dirección | -| telefono | TEXT | Teléfono de contacto | -| email | TEXT | Correo electrónico | -| fecha_creacion | TIMESTAMP | Fecha de creación (default: now) | -| logo_url | TEXT | URL del logo | -| imagen_url | TEXT | URL de imagen principal | - ---- - -## 🏪 `negocio` - -| Columna | Tipo de dato | Descripción | -|-----------------|----------------|-----------------------------------| -| id | UUID | PRIMARY KEY | -| empresa_id | UUID | FK → empresa.id | -| nombre | TEXT | Nombre del negocio | -| direccion | TEXT | Dirección | -| latitud | DECIMAL(9,6) | Latitud geográfica | -| longitud | DECIMAL(9,6) | Longitud geográfica | -| tipo_local | TEXT | Tipo de local (Sucursal, etc.) | -| fecha_creacion | TIMESTAMP | Default: now() | -| logo_url | TEXT | Logo del negocio | -| imagen_url | TEXT | Imagen del negocio | - ---- - -## 🧾 `categoria_componente` - -| Columna | Tipo de dato | Descripción | -|---------|--------------|--------------------------| -| id | SERIAL | PRIMARY KEY | -| nombre | TEXT | Nombre único de categoría| - ---- - -## 📦 `componente` - -| Columna | Tipo de dato | Descripción | -|-----------------|----------------|------------------------------------| -| id | UUID | PRIMARY KEY | -| negocio_id | UUID | FK → negocio.id | -| categoria_id | INT | FK → categoria_componente.id | -| nombre | TEXT | Nombre del componente | -| descripcion | TEXT | Descripción general | -| en_uso | BOOLEAN | Si está en uso | -| activo | BOOLEAN | Si está activo | -| ubicacion | TEXT | Ubicación física (rack, bandeja) | -| imagen_url | TEXT | URL de imagen | -| fecha_registro | TIMESTAMP | Default: now() | -| distribucion_id | UUID | FK → distribucion.id | - ---- - -## 🔌 `detalle_cable` - -| Columna | Tipo de dato | -|----------------|----------------| -| componente_id | UUID (PK, FK) | -| tipo_cable | TEXT | -| color | TEXT | -| tamaño | DECIMAL(5,2) | -| tipo_conector | TEXT | - ---- - -## 📶 `detalle_switch` - -| Columna | Tipo de dato | -|----------------------|----------------| -| componente_id | UUID (PK, FK) | -| marca | TEXT | -| modelo | TEXT | -| numero_serie | TEXT | -| administrable | BOOLEAN | -| poe | BOOLEAN | -| cantidad_puertos | INT | -| velocidad_puertos | TEXT | -| tipo_puertos | TEXT | -| ubicacion_en_rack | TEXT | -| direccion_ip | TEXT | -| firmware | TEXT | - ---- - -## 🧱 `detalle_patch_panel` - -| Columna | Tipo de dato | -|---------------------|----------------| -| componente_id | UUID (PK, FK) | -| tipo_conector | TEXT | -| numero_puertos | INT | -| categoria | TEXT | -| tipo_montaje | TEXT | -| numeracion_frontal | BOOLEAN | -| panel_ciego | BOOLEAN | - ---- - -## 🗄 `detalle_rack` - -| Columna | Tipo de dato | -|------------------------|----------------| -| componente_id | UUID (PK, FK) | -| tipo | TEXT | -| altura_u | INT | -| profundidad_cm | INT | -| ancho_cm | INT | -| ventilacion_integrada | BOOLEAN | -| puertas_con_llave | BOOLEAN | -| ruedas | BOOLEAN | -| color | TEXT | - ---- - -## 🧰 `detalle_organizador` - -| Columna | Tipo de dato | -|--------------|----------------| -| componente_id| UUID (PK, FK) | -| tipo | TEXT | -| material | TEXT | -| tamaño | TEXT | -| color | TEXT | - ---- - -## ⚡ `detalle_ups` - -| Columna | Tipo de dato | -|--------------------|----------------| -| componente_id | UUID (PK, FK) | -| tipo | TEXT | -| marca | TEXT | -| modelo | TEXT | -| voltaje_entrada | TEXT | -| voltaje_salida | TEXT | -| capacidad_va | INT | -| autonomia_minutos | INT | -| cantidad_tomas | INT | -| rackeable | BOOLEAN | - ---- - -## 🔐 `detalle_router_firewall` - -| Columna | Tipo de dato | -|--------------------------|----------------| -| componente_id | UUID (PK, FK) | -| tipo | TEXT | -| marca | TEXT | -| modelo | TEXT | -| numero_serie | TEXT | -| interfaces | TEXT | -| capacidad_routing_gbps | DECIMAL(5,2) | -| direccion_ip | TEXT | -| firmware | TEXT | -| licencias | TEXT | - ---- - -## 🧿 `detalle_equipo_activo` - -| Columna | Tipo de dato | -|-------------------|----------------| -| componente_id | UUID (PK, FK) | -| tipo | TEXT | -| marca | TEXT | -| modelo | TEXT | -| numero_serie | TEXT | -| especificaciones | TEXT | -| direccion_ip | TEXT | -| firmware | TEXT | - ---- - -## 🧭 `distribucion` - -| Columna | Tipo de dato | Descripción | -|--------------|----------------|--------------------------------------| -| id | UUID | PRIMARY KEY | -| negocio_id | UUID | FK → negocio.id | -| tipo | TEXT | 'MDF' o 'IDF' | -| nombre | TEXT | Nombre de la ubicación lógica | -| descripcion | TEXT | Detalles adicionales (opcional) | - ---- - -## 🔗 `conexion_componente` - -| Columna | Tipo de dato | Descripción | -|-----------------------|----------------|------------------------------------------| -| id | UUID | PRIMARY KEY | -| componente_origen_id | UUID | FK → componente.id | -| componente_destino_id | UUID | FK → componente.id | -| descripcion | TEXT | Descripción de la conexión (opcional) | -| activo | BOOLEAN | Si la conexión está activa | - ---- - - - -## 👁️ `vista_negocios_con_coordenadas` - -| Columna | Tipo de dato | Descripción | -|--------------------|--------------|--------------------------------------------| -| negocio_id | UUID | ID del negocio | -| nombre_negocio | TEXT | Nombre del negocio | -| latitud | DECIMAL | Latitud del negocio | -| longitud | DECIMAL | Longitud del negocio | -| logo_negocio | TEXT | URL del logo del negocio | -| imagen_negocio | TEXT | URL de la imagen del negocio | -| empresa_id | UUID | ID de la empresa | -| nombre_empresa | TEXT | Nombre de la empresa | -| logo_empresa | TEXT | URL del logo de la empresa | -| imagen_empresa | TEXT | URL de la imagen de la empresa | - ---- - -## 📋 `vista_inventario_por_negocio` - -| Columna | Tipo de dato | Descripción | -|--------------------|--------------|---------------------------------------------| -| componente_id | UUID | ID del componente | -| nombre_componente | TEXT | Nombre del componente | -| categoria | TEXT | Categoría del componente | -| en_uso | BOOLEAN | Si está en uso | -| activo | BOOLEAN | Si está activo | -| ubicacion | TEXT | Ubicación física del componente | -| imagen_componente | TEXT | Imagen asociada al componente | -| negocio_id | UUID | ID del negocio | -| nombre_negocio | TEXT | Nombre del negocio | -| logo_negocio | TEXT | Logo del negocio | -| imagen_negocio | TEXT | Imagen del negocio | -| empresa_id | UUID | ID de la empresa | -| nombre_empresa | TEXT | Nombre de la empresa | -| logo_empresa | TEXT | Logo de la empresa | -| imagen_empresa | TEXT | Imagen de la empresa | - ---- - -## 🧵 `vista_detalle_cables` - -| Columna | Tipo de dato | Descripción | -|--------------------|--------------|--------------------------------------------| -| componente_id | UUID | ID del componente | -| nombre | TEXT | Nombre del cable | -| tipo_cable | TEXT | Tipo de cable (UTP, fibra, etc.) | -| color | TEXT | Color del cable | -| tamaño | DECIMAL | Longitud del cable | -| tipo_conector | TEXT | Tipo de conector (RJ45, LC, etc.) | -| en_uso | BOOLEAN | Si está en uso | -| activo | BOOLEAN | Si está activo | -| ubicacion | TEXT | Ubicación física | -| imagen_componente | TEXT | Imagen del cable | -| nombre_negocio | TEXT | Nombre del negocio | -| logo_negocio | TEXT | Logo del negocio | -| nombre_empresa | TEXT | Nombre de la empresa | -| logo_empresa | TEXT | Logo de la empresa | - ---- - -## 📊 `vista_resumen_componentes_activos` - -| Columna | Tipo de dato | Descripción | -|------------------|--------------|----------------------------------------------| -| nombre_empresa | TEXT | Nombre de la empresa | -| nombre_negocio | TEXT | Nombre del negocio | -| categoria | TEXT | Categoría del componente | -| cantidad_activos | INTEGER | Cantidad total de componentes activos | - ---- - -## 🔌 `vista_conexiones_por_negocio` - -| Columna | Tipo de dato | Descripción | -|-----------------------|--------------|------------------------------------------| -| id | UUID | ID de la conexión | -| componente_origen_id | UUID | Componente origen | -| componente_destino_id | UUID | Componente destino | -| descripcion | TEXT | Descripción de la conexión | -| activo | BOOLEAN | Si la conexión está activa | -""" diff --git a/assets/referencia/rol_logico_componente.png b/assets/referencia/rol_logico_componente.png new file mode 100644 index 0000000000000000000000000000000000000000..e15dba9d58b2d27ed9fded42eeaeb327faaae3c3 GIT binary patch literal 34823 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 z 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 SgaWVjL`+!fV~L=)*Z%+kBnrL& literal 0 HcmV?d00001 diff --git a/assets/referencia/tablas_nethive.txt b/assets/referencia/tablas_nethive.txt index ddc5f5a..dc13b70 100644 --- a/assets/referencia/tablas_nethive.txt +++ b/assets/referencia/tablas_nethive.txt @@ -161,6 +161,25 @@ tipo (text), nombre (text), descripcion (text), +-- conexion_alimentacion -- + +id (uuid) (PK), +origen_id (uuid) (FK de nethive.componente.id), +destino_id (uuid) (FK de nethive.componente.id), +cable_id (uuid) (FK de nethive.componente.id, opcional), +descripcion (text), +activo (bool) + +-- rol_logico_componente -- + +id (serial) (PK), +nombre (text), +descripcion (text) + + +-- tipo_distribucion -- +id (serial) (PK), +nombre (text) ******* VISTAS: ******* @@ -174,7 +193,7 @@ tamaño (numeric), tipo_conector (text), conexion_id (uuid), --- vista_conexiones_por_cables -- +-- vista_conexiones_con_cables -- conexion_id (uuid), descripcion (text), @@ -275,3 +294,21 @@ ubicacion (text), imagen_url (text), fecha_registro (timestamp), +-- vista_alimentacion_componentes -- + +id (uuid), +origen_id (uuid), +nombre_origen (text), +categoria_origen (int4), + +destino_id (uuid), +nombre_destino (text), +categoria_destino (int4), + +cable_id (uuid), +nombre_cable (text), +categoria_cable (int4), + +descripcion (text), +activo (bool) + diff --git a/assets/referencia/tipo_distribucion.png b/assets/referencia/tipo_distribucion.png new file mode 100644 index 0000000000000000000000000000000000000000..f1e0a48d33fdf4c0f9df3bb172f0762b8785cd08 GIT binary patch literal 12543 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 literal 0 HcmV?d00001 diff --git a/assets/referencia/videos_provider.txt b/assets/referencia/videos_provider.txt deleted file mode 100644 index 34900e5..0000000 --- a/assets/referencia/videos_provider.txt +++ /dev/null @@ -1,1249 +0,0 @@ -import 'dart:convert'; - -import 'dart:typed_data'; -import 'package:auto_size_text/auto_size_text.dart'; -import 'package:file_picker/_internal/file_picker_web.dart'; -import 'package:file_picker/file_picker.dart'; - -import 'package:image_picker/image_picker.dart'; -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; - -import 'package:pluto_grid/pluto_grid.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; -import 'package:path/path.dart' as p; - -import 'package:cbluna_crm_lu/helpers/globals.dart'; -import 'package:cbluna_crm_lu/models/content_manager/ad_by_genre.dart'; -import 'package:cbluna_crm_lu/pages/widgets/toasts/msj_toast.dart'; - -import '../../lib/models/content_manager/all_ads_one_table_model.dart'; -import '../../lib/pages/widgets/video_player_caro.dart'; - -class VideosProvider extends ChangeNotifier { - List listStateManager = []; - PlutoGridStateManager? stateManager; - - final controllerBusqueda = TextEditingController(); - String parametroBusqueda = ""; - - TextEditingController modeloController = TextEditingController(); - - String? videoCoverFile; - String? videoName; - String? videoUrl; - String? videoPath; - - String fileExtension = ''; - String resolucion = "1"; - String area = "7"; - - Uint8List? webVideo; - String? categoryImgFileName; - Uint8List? categoryImageToUpload; - - String? categoryName; - bool noImageToUpload = true; - - //------- - late final TextEditingController filasController; - //------- - - List> listPagos = []; - - final controllerCount = TextEditingController(); - int countI = 0; - int countF = 19; - - final controllerBusquedaFiltro = TextEditingController(); - - bool filtroAvanzado = false; - bool filtroSimple = false; - - /////////////////////////////////////////// - - var tableTop1Gourp = AutoSizeGroup(); - var tableTopGroup = AutoSizeGroup(); - var tableContentGroup = AutoSizeGroup(); - final busquedaVideoController = TextEditingController(); - /////////////////////////////////////////// - late MsjToast toastMessage; - - /// - /// - - List adsByGenre = []; - List> rows = []; - List rows_ = []; - List allVideoRows = []; - List listaQrsSeleccionados = []; - //----------------------------------------------Paginador variables - - int seccionActual = 1; - int totalSecciones = 1; - int totalFilas = 0; - int from = 0; - int hasta = 20; - - //----------------------------------------------Calendario Programacion - - FilePickerResult? docProveedor; - //----------------------------------------------Video data - String? videoCoverFileNameNoModif; - List videoCategories = []; - List videoCategoriesId = []; - List selectedVideoCategories = []; - List selectedVideoCategoriesId = []; - List videoCategoriesImages = []; - - String? posterPath; - String tituloVideo = ''; - String descripcionVideo = ''; - String videoOutlink = ''; - String videoPatner = ''; - Uint8List? videoPoster; - int videoPoints = 0; - Duration duracionVideo = Duration.zero; - int duratinVideoSeconds = 0; - List videoPriority = []; - String? videoPosterPath; - //----------------------------------------------VideoYables - bool showFullVideoTable = true; - List videos = []; - String selectePriority = "baja"; - - int warningCountTablaVideos = 0; - bool showWarningsOnTable = false; - int? selectedVideoId; -//---------------------------------------------- - VideosProvider() { - getVideoPrioritiesList(); - getCategories(); - refreshVideoTablet(); - } - - updateState(AdsByGenre expandir) { - expandir.isExpanded = !expandir.isExpanded; - notifyListeners(); - } - - getVideoData(Duration? duracion, int? duracionSegundos) async { - duracionVideo = duracion ?? Duration.zero; - duratinVideoSeconds = duracionSegundos ?? 0; - } - - setVideoTableFullOrCategories(int index) { - switch (index) { - case 0: - showFullVideoTable = true; - getAllAdsOnOneTable(); - break; - case 1: - showFullVideoTable = false; - getAdsByCategories(); - break; - default: - showFullVideoTable = false; - getAdsByCategories(); - } - notifyListeners(); - return; - } - - getAdsByCategories() async { - try { - final query = supabaseLU.rpc('lu_buscar_videos_por_genero', params: { - 'busqueda': busquedaVideoController.text, - }); - - final res = await query.select(); - - if (res == null) { - return; - } - - adsByGenre = (res as List) - .map((ad) => AdsByGenre.fromJson(jsonEncode(ad))) - .toList(); - - rows_ = []; - rows_.clear(); - rows.clear(); - - for (var genre in adsByGenre) { - adsByGenre[adsByGenre.indexOf(genre)].videoCount = genre.videos.length; - for (var video in genre.videos) { - rows_.add(PlutoRow(cells: { - 'video_id': PlutoCell(value: video.videoId.toString()), - 'posterPath': PlutoCell( - value: video.posterFileName != null - ? "${supabase.storage.from('lectores_urb/imagenes/videos/posters').getPublicUrl(video.posterFileName)}?${DateTime.now().millisecondsSinceEpoch}" - : ''), - 'status': PlutoCell(value: video.status ?? 'neutra'), - 'priority': PlutoCell(value: video.priority), - 'title': PlutoCell(value: video.title), - 'urlAd': PlutoCell(value: video.urlAd ?? ''), - 'overview': PlutoCell(value: video.overview), - 'points': PlutoCell(value: 1), - 'expirationDate': PlutoCell(value: video.expirationDate ?? ''), - 'created_at': PlutoCell(value: video.createdAt ?? ''), - 'durationVideo': PlutoCell(value: video.duration ?? ''), - 'video_url': PlutoCell( - value: video.videoUrl != null - ? "${supabase.storage.from('lectores_urb/videos/cm_videos').getPublicUrl(video.videoFileName.toString())}?${DateTime.now().millisecondsSinceEpoch}" - : ''), - 'editar': PlutoCell(value: video.videoId), - 'eliminar': PlutoCell(value: video.videoId), - 'video_file_name': PlutoCell(value: video.videoFileName.toString()), - 'poster_file_name': - PlutoCell(value: video.posterFileName.toString()), - 'patner': PlutoCell(value: video.partner ?? ''), - 'categories': PlutoCell(value: video.categories ?? ''), - 'video_status': PlutoCell(value: video.videoStatus), - })); - } - rows.add(rows_); - rows_ = []; - } - showWarningsOnTable = false; - notifyListeners(); - return; - } catch (e) { - print('error en getAdsByCategories(): ${e.toString()}'); - } - notifyListeners(); - return; - } - - getAdsByCategoriesWarnings() async { - try { - final query = supabaseLU.rpc('lu_buscar_videos_por_genero', params: { - 'busqueda': busquedaVideoController.text, - }); - - final res = await query.select(); - - if (res == null) { - return; - } - - adsByGenre = (res as List) - .map((ad) => AdsByGenre.fromJson(jsonEncode(ad))) - .toList(); - - rows_ = []; - rows_.clear(); - rows.clear(); - for (var genre in adsByGenre) { - genre.videoCount = 0; - for (var video in genre.videos!) { - if (video.categories[0] == [null] || - video.categories[0].isEmpty || - video.categories[0] == [""] || - video.categories[0] == ["null"] || - video.urlAd == null || - video.urlAd == "" || - video.urlAd == "null" || - video.overview == "" || - video.overview == "null" || - video.partner == "" || - video.partner == null) { - rows_.add(PlutoRow(cells: { - 'video_id': PlutoCell(value: video.videoId.toString()), - 'posterPath': PlutoCell( - value: video.posterFileName != null - ? "${supabase.storage.from('lectores_urb/imagenes/videos/posters').getPublicUrl(video.posterFileName)}?${DateTime.now().millisecondsSinceEpoch}" - : 'https://placehold.co/150x150'), - 'status': PlutoCell(value: video.status ?? 'neutra'), - 'priority': PlutoCell(value: video.priority ?? '4'), - 'title': PlutoCell(value: video.title), - 'urlAd': PlutoCell(value: video.urlAd ?? ''), - 'overview': PlutoCell(value: video.overview), - 'points': PlutoCell(value: video.points ?? ''), - 'expirationDate': PlutoCell(value: video.expirationDate ?? ''), - 'created_at': PlutoCell(value: video.createdAt ?? ''), - 'durationVideo': PlutoCell(value: video.duration ?? ''), - 'video_url': PlutoCell( - value: video.videoUrl != null - ? "${supabase.storage.from('lectores_urb/videos/cm_videos').getPublicUrl(video.videoFileName.toString())}?${DateTime.now().millisecondsSinceEpoch}" - : ''), - 'editar': PlutoCell(value: video.videoId), - 'eliminar': PlutoCell(value: video.videoId), - 'video_file_name': - PlutoCell(value: video.videoFileName.toString()), - 'poster_file_name': - PlutoCell(value: video.posterFileName.toString()), - 'patner': PlutoCell(value: video.partner ?? ''), - 'categories': PlutoCell(value: video.categories ?? ''), - 'video_status': PlutoCell(value: video.videoStatus), - })); - genre.videoCount += 1; - } - } - rows.add(rows_); - rows_ = []; - } - - showWarningsOnTable = true; - notifyListeners(); - return; - } catch (e) { - print('error en getAdsCategoriesWarning: ${e.toString()}'); - } - } - - getAllAdsOnOneTable([bool warningMode = false]) async { - try { - final String query = busquedaVideoController.text.trim(); - - final res = await supabaseLU.rpc('lu_buscar_videos', params: { - 'busqueda': query.isEmpty ? null : query, - 'warningmode': false, - }); - - if (res == null) { - print("--X---Error: respuesta nula al llamar RPC."); - return; - } - - try { - videos = (res as List) - .map((ad) => AllAdsOneTableModel.fromJson(jsonEncode(ad))) - .toList(); - } catch (e) { - print("Error al parsear lista de videos: ${e.toString()}"); - } - - allVideoRows.clear(); - - // El warning count total lo devuelve cada fila, tomamos el primero - warningCountTablaVideos = - videos.isNotEmpty ? videos.first.warningCount : 0; - - for (AllAdsOneTableModel video in videos) { - allVideoRows.add(PlutoRow(cells: { - 'video_id': PlutoCell(value: video.id), - 'poster_path': PlutoCell( - value: video.posterFileName != null - ? "${supabase.storage.from('lectores_urb/imagenes/videos/posters').getPublicUrl(video.posterFileName)}?${DateTime.now().millisecondsSinceEpoch}" - : 'https://placehold.co/150x150'), - 'priority': PlutoCell(value: video.priority), - 'title': PlutoCell(value: video.title), - 'url_ad': PlutoCell(value: video.urlAd ?? ''), - 'overview': PlutoCell(value: video.overview), - 'points': PlutoCell(value: video.points), - 'expiration_date': PlutoCell(value: video.expirationDate), - 'created_at': PlutoCell(value: video.createdAt), - 'duration_video': PlutoCell(value: video.durationVideo), - 'video_url': PlutoCell( - value: video.video.isNotEmpty - ? "${video.video}?${DateTime.now().millisecondsSinceEpoch}" - : '', - ), - 'editar': PlutoCell(value: video.id), - 'eliminar': PlutoCell(value: video.id), - 'video_file_name': PlutoCell(value: video.videoFileName ?? ''), - 'poster_file_name': PlutoCell(value: video.posterFileName ?? ''), - 'partner': PlutoCell(value: video.partner ?? ''), - 'categories': PlutoCell(value: video.categories), - 'video_status': PlutoCell(value: video.videoStatus), - 'qr_codes': PlutoCell( - value: video.qrCodes.expand((entry) { - try { - final details = entry['details']; - - // Verifica si 'details' está presente y tiene la clave 'qrs' - if (details != null && - details is Map && - details['qrs'] is List) { - return (details['qrs'] as List) - .map((qrsEntry) => qrsEntry['qrs_concat']) - .whereType(); - } - } catch (e) { - // Si ocurre un error al intentar acceder a los datos, lo manejamos de forma silenciosa - print('Error al acceder a QR details: $e'); - } - return []; // Devuelve una lista vacía en caso de error o datos inválidos - }).join('\n'), // o ', ' si prefieres en una sola línea - ), - 'qr_codes_list': PlutoCell( - value: video.qrCodes.expand((entry) { - try { - final details = entry['details']; - if (details != null && - details is Map && - details['qrs'] is List) { - return (details['qrs'] as List) - .map((qrsEntry) => qrsEntry['qrs_concat']) - .whereType(); - } - } catch (e) { - print('Error al acceder a QR details (lista): $e'); - } - return []; - }).toList(), // ← clave aquí - ), - })); - } - - showWarningsOnTable = false; - notifyListeners(); - } catch (e) { - print('Error en getAllAdsOnOneTable: ${e.toString()}'); - notifyListeners(); - } - } - - getAllAdsOnOneTableWarnings() async { - try { - final res = await supabaseLU.from('videos_view').select(); - if (res == null) { - print("--X---Error: ${res.error}"); - return; - } - - videos = (res as List) - .map((ad) => AllAdsOneTableModel.fromJson(jsonEncode(ad))) - .toList(); - - allVideoRows.clear(); - - for (AllAdsOneTableModel video in videos) { - if (video.categories[0] == [null] || - video.categories[0] == null || - video.categories[0] == [] || - video.categories[0] == [""] || - video.categories[0] == ["null"] || - video.urlAd == "" || - video.urlAd == null || - video.urlAd == "null" || - video.overview == "" || - video.overview == "null" || - video.partner == "" || - video.partner == null) { - allVideoRows.add(PlutoRow(cells: { - 'id': PlutoCell(value: video.id), - 'posterPath': PlutoCell( - value: video.posterFileName != null - ? "${supabase.storage.from('lectores_urb/imagenes/videos/posters').getPublicUrl(video.posterFileName)}?${DateTime.now().millisecondsSinceEpoch}" - : 'https://placehold.co/150x150'), - 'priority': PlutoCell(value: video.priority ?? '4'), - 'title': PlutoCell(value: video.title ?? ''), - 'urlAd': PlutoCell(value: video.urlAd.toString() ?? ''), - 'overview': PlutoCell(value: video.overview ?? ''), - 'points': PlutoCell(value: video.points ?? ''), - 'expirationDate': PlutoCell(value: video.expirationDate ?? ''), - 'created_at': PlutoCell(value: video.createdAt ?? ''), - 'durationVideo': PlutoCell(value: video.durationVideo ?? ''), - 'video_url': PlutoCell( - value: - "${supabase.storage.from('lectores_urb/videos/cm_videos').getPublicUrl(video.videoFileName.toString())}?${DateTime.now().millisecondsSinceEpoch}"), - 'editar': PlutoCell(value: video.id), - 'eliminar': PlutoCell(value: video.id), - 'video_file_name': PlutoCell(value: video.videoFileName ?? ''), - 'poster_file_name': PlutoCell(value: video.posterFileName ?? ''), - 'patner': PlutoCell(value: video.partner ?? ''), - 'categories': PlutoCell(value: video.categories ?? ''), - 'video_status': PlutoCell(value: video.videoStatus), - })); - } - } - showWarningsOnTable = true; - notifyListeners(); - return; - } catch (e) { - print('error en getAllAdsOnOneTableWarnings: ${e.toString()}'); - } - notifyListeners(); - return; - } - - Future> getCategories() async { - try { - final res = await supabaseLU - .from('video_genero') - .select('name, id, poster_image_file') - .eq('visible', true) - .order('name', ascending: true); - - videoCategories = (res as List) - .map((category) => category['name'] as String) - .toList(); - - videoCategoriesId = (res as List) - .map((category) => category['id'] as int) - .toList(); - - videoCategoriesImages = (res as List) - .map((category) => category['poster_image_file'] as String) - .toList(); - - return videoCategories; - } catch (e) { - print("ERROR en getVideosVinculados: " + e.toString()); - notifyListeners(); - return []; - } - } - - //usando el id del video cambiar su estado "video_status" a true o false - cambiarEstadoVideo(int idVideo, bool estado) async { - await supabaseLU - .from('videos') - .update({'video_status': estado}) - .eq('id', idVideo) - .select(); - } - -/* ------------------------------------------------------------------------- */ -/* ------------------------------------------------------------------------- */ -/* ------------------------------------------------------------------------- */ -/* ------------------------------------------------------------------------- */ -/* ------------------------------------------------------------------------- */ - - Widget? getPosterImage(dynamic image, - {double height = 180, BoxFit boxFit = BoxFit.cover}) { - if (image == null) { - return Image.asset('assets/images/placeholder_no_image.jpg'); - } else if (image is Uint8List) { - return Image.memory( - image, - height: height, - width: double.infinity, - fit: boxFit, - ); - } else if (image is String) { - return Image.network( - image, - height: height, - width: double.infinity, - filterQuality: FilterQuality.high, - fit: boxFit, - ); - } else { - return Image.asset('assets/images/placeholder_no_image.jpg'); - } - } - - Widget? getCategoryImage(dynamic image, - {double height = 180, BoxFit boxFit = BoxFit.cover}) { - if (image == null) { - noImageToUpload = true; - - return Image.asset('assets/images/placeholder_upload_image.png'); - } else if (image is Uint8List) { - noImageToUpload = false; - - return Image.memory( - image, - height: height, - width: double.infinity, - fit: boxFit, - ); - } else if (image is String) { - noImageToUpload = false; - - return Image.network( - "${supabase.storage.from('lectores_urb/imagenes/videos/categories').getPublicUrl(image)}?${DateTime.now().millisecondsSinceEpoch}", - height: height, - width: double.infinity, - filterQuality: FilterQuality.high, - fit: boxFit, - ); - } else { - noImageToUpload = true; - - return Image.asset('assets/images/placeholder_upload_image.png'); - } - } - - //get video widget VideoScreenNew; - - Widget? getVideoWidget(dynamic image, - {double height = 180, width = 500, BoxFit boxFit = BoxFit.contain}) { - if (videoPath == null) { - return Image.asset('assets/images/placeholder_no_video.png', - width: width, height: height, fit: boxFit); - } else { - return VideoScreenNew( - videoUrl: videoPath, - ); - } - } - -/////////////CATEGORY/////////////////////// - - Future selectCategoryImage() async { - categoryImgFileName = null; - categoryImageToUpload = null; - FilePickerResult? picker = await FilePickerWeb.platform - .pickFiles(type: FileType.custom, allowedExtensions: ['jpg', 'png']); - - //get and load pdf - if (picker != null) { - var now = DateTime.now(); - var formatter = DateFormat('yyyyMMddHHmmss'); - var timestamp = formatter.format(now); - - categoryImgFileName = 'category-$timestamp-${picker.files.single.name}'; - categoryImageToUpload = picker.files.single.bytes; - noImageToUpload = false; - } else { - categoryImgFileName = null; - categoryImageToUpload = null; - } - - notifyListeners(); - return; - } - - uploadCategoryImage() async { - if (categoryImageToUpload != null && categoryImgFileName != null) { - await supabase.storage - .from('lectores_urb/imagenes/videos/categories') - .uploadBinary( - categoryImgFileName!, - categoryImageToUpload!, - fileOptions: const FileOptions( - cacheControl: '3600', - upsert: false, - ), - ); - - return; - } - return null; - } - - Future registrarNuevaCategoria() async { - await uploadCategoryImage(); - - final res = await supabaseLU.from('video_genero').insert( - { - 'name': categoryName, - 'poster_image_file': categoryImgFileName, - }, - ).select(); - - if (res == null) { - print("Error al registrar categoria"); - print(res.error!.message); - return false; - } - - return true; - } - -/////////////ACTUALIZAR CATEGORIA/////////////////////// - - Future updateCategory(int idCategory, String? name) async { - if (categoryImageToUpload == null) { - final res = await supabaseLU - .from('video_genero') - .update( - { - 'name': name.toString(), - }, - ) - .eq('id', idCategory) - .select(); - - if (res == null) { - print("Error al registrar updateCategory1"); - print(res.error!.message); - return false; - } - return true; - } -//--------------------------------------------------------------- - final getFileName = await supabaseLU - .from('video_genero') - .select('poster_image_file') - .eq('id', idCategory) - .single(); - - if (getFileName['poster_image_file'] != null && - categoryImageToUpload != null) { - await supabase.storage - .from('lectores_urb/imagenes/videos/categories') - .updateBinary( - getFileName['poster_image_file'], - categoryImageToUpload!, - fileOptions: const FileOptions( - cacheControl: '3600', - upsert: false, - ), - ); - } else { - print("NO EXISTE SUBIR"); - await supabase.storage - .from('lectores_urb/imagenes/videos/categories') - .uploadBinary( - categoryImgFileName!, - categoryImageToUpload!, - fileOptions: const FileOptions( - cacheControl: '3600', - upsert: false, - ), - ); - await supabaseLU - .from('video_genero') - .update( - { - 'poster_image_file': categoryImgFileName, - }, - ) - .eq('id', idCategory) - .select(); - } - -//--------------------------------------------------------------- - return true; - } - -/////////////BORRAR CATEGORIA/////////////////////// - - Future deleteCategory(int idCategory, String? filename) async { - await supabaseLU - .from('video_in_genero') - .delete() - .eq('genero_id', idCategory) - .select(); - - await supabase.storage - .from('lectores_urb') - .remove(["imagenes/videos/categories/$filename"]); - - await supabaseLU - .from('video_genero') - .delete() - .eq('id', idCategory) - .select(); - categoryImgFileName = null; - - resetAllvideoData(); - getCategories(); - refreshVideoTablet(); - - return true; - } - -//---------------ATUALIZAR VIDEOS------------------ - - Future updateVideoData( - int idVideo, - String title, - String description, - String partner, - String outlink, - int points, - List selectedVideoCategoriesId, { - String? videoImage, - String? posterImage, - String? posterFileName, - List? qrList, - }) async { -/* print("updateVideoData"); - print("idVideo: $idVideo"); - print("title: $title"); - print("description: $description"); - print("partner: $partner"); - print("outlink: $outlink"); - print("points: $points"); - print("selectedVideoCategoriesId: $selectedVideoCategoriesId"); - print("posterFileName: $posterFileName"); - print("posterImage: $posterImage"); - print("videoImage: $videoImage"); - print("qrList: $qrList"); */ - - if (qrList != null) { - final existingQrs = await getQrsByVideoId(idVideo); - // Eliminar los QRs que no están en qrList - for (String qr in existingQrs) { - if (!qrList.contains(qr)) { - await supabaseLU - .from('video_in_qr') - .delete() - .eq('id_qr_fk', qr) - .eq('id_video_fk', idVideo) - .select(); - } - } - - // Agregar nuevos QRs - for (String qr in qrList) { - if (!existingQrs.contains(qr)) { - await supabaseLU.from('video_in_qr').insert({ - 'id_qr_fk': qr, - 'id_video_fk': idVideo, - }).select(); - } - } - } - - final priorityrecibe = await getPriorityId(selectePriority); - final res = await supabaseLU - .from('videos') - .update( - { - 'title': title.toString(), - 'overview': description.toString(), - 'url_ad': outlink.toString(), - 'partner': partner.toString(), - 'points': 1, - 'priority': priorityrecibe, - 'video_status': true, - }, - ) - .eq('id', idVideo) - .select(); - - if (res == null) { - print("Error al registrar updateVideoTitulo"); - print(res.error!.message); - resetAllvideoData(); - return false; - } - - if (webVideo != null) { - final getFileName = await supabaseLU - .from('videos') - .select('video_file_name') - .eq('id', idVideo) - .single(); - - print("getFileName: $getFileName"); - if (getFileName['video_file_name'] != null) { - await supabase.storage - .from('lectores_urb/videos/cm_videos') - .updateBinary( - getFileName['video_file_name'], - webVideo!, - fileOptions: const FileOptions( - cacheControl: '3600', - upsert: false, - ), - ); - - webVideo = null; - } - } - - if (videoPoster != null) { - await supabase.storage - .from('lectores_urb/imagenes/videos/posters') - .updateBinary(videoCoverFileNameNoModif.toString(), videoPoster!); - } - await supabaseLU - .from('video_in_genero') - .delete() - .eq('video_id', idVideo) - .select(); - await registrarCategoriasDelVideoUsandoId( - idVideo, selectedVideoCategoriesId); - - resetAllvideoData(); - - refreshVideoTablet(); - return true; - } - -/////////////VIDEOS/////////////////////// - - getVideoPrioritiesList() async { - final res = await supabaseLU.from('video_priority').select("name"); - - videoPriority = (res as List) - .map((priority) => priority['name'] as String) - .toList(); - - return videoPriority; - } - - Future uploadPosterImage() async { - if (videoPoster != null && videoCoverFile != null) { - await supabase.storage - .from('lectores_urb/imagenes/videos/posters') - .uploadBinary( - '$videoName-$videoCoverFile', - videoPoster!, - fileOptions: const FileOptions( - cacheControl: '3600', - upsert: false, - ), - ); - - final res = await supabase.storage - .from('lectores_urb/imagenes/videos/posters') - .getPublicUrl('$videoName-$videoCoverFile'); - - posterPath = res; - - return videoCoverFile; - } - return null; - } - - Future selectPosterImage() async { - videoCoverFile = null; - videoPoster = null; - FilePickerResult? picker = await FilePickerWeb.platform - .pickFiles(type: FileType.custom, allowedExtensions: ['jpg', 'png']); - - //get and load pdf - if (picker != null) { - var now = DateTime.now(); - var formatter = DateFormat('yyyyMMddHHmmss'); - var timestamp = formatter.format(now); - - videoCoverFile = 'poster-$timestamp-${picker.files.single.name}'; - videoPoster = picker.files.single.bytes; - } else { - videoCoverFile = null; - videoPoster = null; - } - - notifyListeners(); - return; - } - - Future selectVideo() async { - videoName = ""; - - final ImagePicker picker = ImagePicker(); - - final XFile? pickedVideo = await picker.pickVideo( - source: ImageSource.gallery, - ); - - if (pickedVideo == null) return false; - - fileExtension = p.extension(pickedVideo.name); - - videoPath = pickedVideo.path; - videoName = pickedVideo.name; - videoName = videoName!.replaceAll(fileExtension, ""); - webVideo = await pickedVideo.readAsBytes(); - - return true; - } - - Future uploadVideo(List categoryID, {List? qrList}) async { - try { - if (webVideo != null && videoName != null) { - final files = await supabase.storage - .from('lectores_urb') - .list(path: 'videos/cm_videos'); - - // Verificar si el archivo ya existe - final fileExists = - files.any((file) => file.name == '$videoName$fileExtension'); - - if (fileExists) { - toastMessage = MsjToast( - message: 'video already exists', - color: Color.fromARGB(255, 236, 187, 50), - ); - return false; - } - - if (videoCoverFile == null) { - toastMessage = MsjToast( - message: 'No poster selected, upload an image to continue', - color: Color.fromARGB(255, 236, 187, 50), - ); - - return false; - } - - //check if videoCoverFile ya existe en storage - final filesPoster = await supabase.storage - .from('lectores_urb') - .list(path: 'imagenes/videos/posters'); - - // Verificar si el archivo ya existe - final fileExistsPoster = filesPoster - .any((file) => file.name == '$videoName-$videoCoverFile'); - - if (fileExistsPoster) { - toastMessage = MsjToast( - message: 'poster file name already exists, (change the name)', - color: Color.fromARGB(255, 236, 187, 50), - ); - return false; - } - - final storageResponse = await supabase.storage - .from('lectores_urb/videos/cm_videos') - .uploadBinary( - '$videoName$fileExtension', - webVideo!, - fileOptions: const FileOptions( - cacheControl: '3600', - upsert: false, - ), - ); - - if (storageResponse == null) return false; - - dynamic res = supabase.storage - .from('lectores_urb/videos/cm_videos') - .getPublicUrl('$videoName$fileExtension'); - videoUrl = res; - - if (await registrarVideo2(categoryID, qrList: qrList)) { - return true; - } else { - return false; - } - } - return false; - } catch (e) { - print("Error en uploadVideo: $e"); - return false; - } - } - - //get priority id using name - Future getPriorityId(String priorityName) async { - final res = await supabaseLU - .from('video_priority') - .select("id") - .eq("name", priorityName) - .limit(1); - - if (res == null) { - print("Error al obtener id de prioridad"); - print(res.error!.message); - return 0; - } - - return res[0]['id']; - } - - Future registrarCategoriasDelVideoUsandoId( - int idVideo, List listaIdsCategorias) async { - try { - await supabaseLU.rpc('lu_añadir_generos_al_video', params: { - 'lista_id_generos': listaIdsCategorias, - 'videoid': idVideo - }).select(); - - return true; - } catch (e) { - print("Error al registrar Categorias del video"); - print(e.toString()); - return false; - } - } - - Future obtenerIdVideoUsandoNombreArchivoStorage( - String videoPath) async { - //obtiene el id del video recien registrado usando su archivo cargado - final res = await supabaseLU - .from('videos') - .select("id") - .eq("video_file_name", videoPath) - .limit(1); - - if (res == null) { - print("Error al al obtener id del video"); - print(res.error!.message); - return false; - } - - await registrarCategoriasDelVideoUsandoId( - res[0]['id'], selectedVideoCategoriesId); - - return true; - } - - Future deleteVideo2( - int idVideo, String videoFileName, String posterFileName) async { - try { - // Llamada al RPC para eliminar el video y obtener los nombres de archivos - final res = await supabaseLU - .rpc('lu_borrar_video', params: {'id_video': idVideo}); - - // Validar la respuesta - if (res == null || res.isEmpty) { - print("El video no pudo ser eliminado o no existe."); - refreshVideoTablet(); - return false; - } - await supabaseLU.from('videos').delete().eq('id', idVideo).select(); - await eliminarVideo(videoFileName, posterFileName); - - refreshVideoTablet(); - return true; - } catch (e) { - print("Error al eliminar el video: ${e.toString()}"); - refreshVideoTablet(); - return false; - } - } - - Future eliminarVideo( - String videoFileName, String posterFileName) async { - await supabase.storage - .from('lectores_urb') - .remove(['videos/cm_videos/$videoFileName']); - await supabase.storage - .from('lectores_urb') - .remove(['imagenes/videos/posters/$posterFileName']); - - return true; - } - - Future>> getGroupedQRCodesByCustomer() async { - try { - // Llamada al RPC en Supabase - final res = await supabaseLU.rpc('get_qr_codes_grouped_by_customer'); - - // Validar el resultado - if (res == null || res.isEmpty) { - print("No se encontraron datos agrupados."); - return []; - } - - // Convertir los resultados en una lista de Map - return List>.from(res).map((group) { - return { - 'row_number': group['row_number'], - 'customer_fk': group['customer_fk'], - 'customer_name': group['customer_name'], - 'details': group['details'], // JSONB con los detalles - }; - }).toList(); - } catch (e) { - print("Error en getGroupedQRCodesByCustomer: ${e.toString()}"); - return []; // Retornar una lista vacía en caso de error - } - } - - Future> getQrsByVideoId(int videoId) async { - try { - final response = await supabaseLU - .from('video_in_qr') - .select('id_qr_fk') - .eq('id_video_fk', videoId); - - if (response == null) { - print('Error al obtener getQrsByCouponId()'); - return []; - } - - return List.from( - (response as List).map((item) => item['id_qr_fk']), - ); - } catch (e) { - print("Error en getQrsByVideoId: ${e.toString()}"); - return []; // Retornar una lista vacía en caso de error - } - } - - void resetAllvideoData() { - videoPoster = null; - videoCoverFile = null; - videoPath = null; - videoName = null; - selectedVideoCategoriesId = []; - selectedVideoCategories = []; - categoryImgFileName = null; - categoryImageToUpload = null; - noImageToUpload = true; - selectePriority = "baja"; - videoOutlink = ""; - videoPoints = 0; - descripcionVideo = ""; - videoPatner = ""; - videoUrl = null; - webVideo = null; - posterPath = null; - videoPosterPath = null; - tituloVideo = ""; - return; - } - - void clearPosterImage() { - videoPoster = null; - videoCoverFile = null; - videoPosterPath = null; - - notifyListeners(); - return; - } - - void clearControllers() { - modeloController.clear(); - notifyListeners(); - } - - void updatestateManager() { - print(videoName); - - notifyListeners(); - } - - void clearVideo() { - videoPath = null; - webVideo = null; - notifyListeners(); - } - - void refreshVideoTablet() { - print(showWarningsOnTable); - if (showWarningsOnTable && warningCountTablaVideos > 0) { - showFullVideoTable == true - ? getAllAdsOnOneTableWarnings() - : getAdsByCategoriesWarnings(); - } else { - showFullVideoTable == true ? getAllAdsOnOneTable() : getAdsByCategories(); - } - } - - Future registrarVideo2(List categoryIds, - {List? qrList}) async { - await uploadPosterImage(); - final priorityrecibe = await getPriorityId(selectePriority); - - final res = await supabaseLU.rpc( - 'lu_registrar_videos_master', - params: { - 'p_title': tituloVideo, - 'p_overview': descripcionVideo, - 'p_video_url': videoUrl, - 'p_url_ad': videoOutlink, - 'p_poster_path': posterPath ?? - "https://u-supabase.virtalus.cbluna-dev.com/storage/v1/object/public/assets/placeholder_no_image.jpg", - 'p_poster_file_name': '$videoName-$videoCoverFile', - 'p_video_file_name': '$videoName$fileExtension', - 'p_duration_video': duratinVideoSeconds, - 'p_partner': videoPatner, - 'p_priority': priorityrecibe, - 'p_qr_list': qrList, - 'p_category_ids': categoryIds - }, - ).select(); - - if (res == null) { - print("Error al registrar video"); - print(res.error!.message); - return false; - } - - if (await obtenerIdVideoUsandoNombreArchivoStorage( - '$videoName$fileExtension')) { - videoName = ""; - descripcionVideo = ""; - refreshVideoTablet(); - return true; - } else { - videoName = ""; - descripcionVideo = ""; - refreshVideoTablet(); - return false; - } - } -} diff --git a/lib/models/nethive/componente_model.dart b/lib/models/nethive/componente_model.dart index 3c97f03..267719e 100644 --- a/lib/models/nethive/componente_model.dart +++ b/lib/models/nethive/componente_model.dart @@ -11,6 +11,8 @@ class Componente { final String? ubicacion; final String? imagenUrl; final DateTime fechaRegistro; + final String? distribucionId; // ← Nuevo (si lo usas) + final String? rolLogicoId; // ← NUEVO Componente({ required this.id, @@ -23,7 +25,8 @@ class Componente { this.ubicacion, this.imagenUrl, required this.fechaRegistro, - String? distribucionId, + this.distribucionId, + this.rolLogicoId, }); factory Componente.fromMap(Map map) { @@ -38,6 +41,8 @@ class Componente { ubicacion: map['ubicacion'], imagenUrl: map['imagen_url'], fechaRegistro: DateTime.parse(map['fecha_registro']), + distribucionId: map['distribucion_id'], + rolLogicoId: map['rol_logico_id'], ); } @@ -53,10 +58,13 @@ class Componente { 'ubicacion': ubicacion, 'imagen_url': imagenUrl, 'fecha_registro': fechaRegistro.toIso8601String(), + 'distribucion_id': distribucionId, + 'rol_logico_id': rolLogicoId, }; } factory Componente.fromJson(String source) => Componente.fromMap(json.decode(source)); + String toJson() => json.encode(toMap()); } diff --git a/lib/models/nethive/conexion_alimentacion_model.dart b/lib/models/nethive/conexion_alimentacion_model.dart new file mode 100644 index 0000000..61b4c0a --- /dev/null +++ b/lib/models/nethive/conexion_alimentacion_model.dart @@ -0,0 +1,39 @@ +class ConexionAlimentacion { + final String id; + final String origenId; + final String destinoId; + final String? cableId; + final String? descripcion; + final bool activo; + + ConexionAlimentacion({ + required this.id, + required this.origenId, + required this.destinoId, + this.cableId, + this.descripcion, + required this.activo, + }); + + factory ConexionAlimentacion.fromMap(Map map) { + return ConexionAlimentacion( + id: map['id'] ?? '', + origenId: map['origen_id'] ?? '', + destinoId: map['destino_id'] ?? '', + cableId: map['cable_id'], + descripcion: map['descripcion'], + activo: map['activo'] ?? false, + ); + } + + Map toMap() { + return { + 'id': id, + 'origen_id': origenId, + 'destino_id': destinoId, + 'cable_id': cableId, + 'descripcion': descripcion, + 'activo': activo, + }; + } +} diff --git a/lib/models/nethive/rol_logico_componente_model.dart b/lib/models/nethive/rol_logico_componente_model.dart new file mode 100644 index 0000000..48a3d89 --- /dev/null +++ b/lib/models/nethive/rol_logico_componente_model.dart @@ -0,0 +1,58 @@ +class RolLogicoComponente { + final int id; + final String nombre; + final String? descripcion; + + RolLogicoComponente({ + required this.id, + required this.nombre, + this.descripcion, + }); + + factory RolLogicoComponente.fromMap(Map map) { + return RolLogicoComponente( + id: map['id'] ?? 0, + nombre: map['nombre'] ?? '', + descripcion: map['descripcion'], + ); + } + + Map toMap() { + return { + 'id': id, + 'nombre': nombre, + 'descripcion': descripcion, + }; + } + + RolLogicoComponente copyWith({ + int? id, + String? nombre, + String? descripcion, + }) { + return RolLogicoComponente( + id: id ?? this.id, + nombre: nombre ?? this.nombre, + descripcion: descripcion ?? this.descripcion, + ); + } + + @override + String toString() { + return 'RolLogicoComponente(id: $id, nombre: $nombre, descripcion: $descripcion)'; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is RolLogicoComponente && + other.id == id && + other.nombre == nombre && + other.descripcion == descripcion; + } + + @override + int get hashCode { + return id.hashCode ^ nombre.hashCode ^ descripcion.hashCode; + } +} diff --git a/lib/models/nethive/tipo_distribucion_model.dart b/lib/models/nethive/tipo_distribucion_model.dart new file mode 100644 index 0000000..1889be2 --- /dev/null +++ b/lib/models/nethive/tipo_distribucion_model.dart @@ -0,0 +1,51 @@ +class TipoDistribucion { + final int id; + final String nombre; + + TipoDistribucion({ + required this.id, + required this.nombre, + }); + + factory TipoDistribucion.fromMap(Map map) { + return TipoDistribucion( + id: map['id'] ?? 0, + nombre: map['nombre'] ?? '', + ); + } + + Map toMap() { + return { + 'id': id, + 'nombre': nombre, + }; + } + + TipoDistribucion copyWith({ + int? id, + String? nombre, + }) { + return TipoDistribucion( + id: id ?? this.id, + nombre: nombre ?? this.nombre, + ); + } + + @override + String toString() { + return 'TipoDistribucion(id: $id, nombre: $nombre)'; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is TipoDistribucion && + other.id == id && + other.nombre == nombre; + } + + @override + int get hashCode { + return id.hashCode ^ nombre.hashCode; + } +} diff --git a/lib/models/nethive/topologia_completa_model.dart b/lib/models/nethive/topologia_completa_model.dart new file mode 100644 index 0000000..ba26446 --- /dev/null +++ b/lib/models/nethive/topologia_completa_model.dart @@ -0,0 +1,199 @@ +import 'package:nethive_neo/models/nethive/componente_model.dart'; +import 'package:nethive_neo/models/nethive/conexion_componente_model.dart'; +import 'package:nethive_neo/models/nethive/conexion_alimentacion_model.dart'; + +class TopologiaCompleta { + final List componentes; + final List conexionesDatos; + final List conexionesEnergia; + + TopologiaCompleta({ + required this.componentes, + required this.conexionesDatos, + required this.conexionesEnergia, + }); + + factory TopologiaCompleta.fromJson(Map json) { + return TopologiaCompleta( + componentes: (json['componentes'] as List? ?? []) + .map((c) => ComponenteTopologia.fromMap(c)) + .toList(), + conexionesDatos: (json['conexiones_datos'] as List? ?? []) + .map((cd) => ConexionDatos.fromMap(cd)) + .toList(), + conexionesEnergia: (json['conexiones_energia'] as List? ?? []) + .map((ce) => ConexionEnergia.fromMap(ce)) + .toList(), + ); + } +} + +class ComponenteTopologia { + final String id; + final String nombre; + final int categoriaId; + final String categoria; + final String? descripcion; + final String? ubicacion; + final String? imagenUrl; + final bool enUso; + final bool activo; + final DateTime fechaRegistro; + final String? distribucionId; + final String? tipoDistribucion; + final String? nombreDistribucion; + + ComponenteTopologia({ + required this.id, + required this.nombre, + required this.categoriaId, + required this.categoria, + this.descripcion, + this.ubicacion, + this.imagenUrl, + required this.enUso, + required this.activo, + required this.fechaRegistro, + this.distribucionId, + this.tipoDistribucion, + this.nombreDistribucion, + }); + + factory ComponenteTopologia.fromMap(Map map) { + return ComponenteTopologia( + id: map['id'] ?? '', + nombre: map['nombre'] ?? '', + categoriaId: map['categoria_id'] ?? 0, + categoria: map['categoria'] ?? '', + descripcion: map['descripcion'], + ubicacion: map['ubicacion'], + imagenUrl: map['imagen_url'], + enUso: map['en_uso'] ?? false, + activo: map['activo'] ?? false, + fechaRegistro: + DateTime.tryParse(map['fecha_registro']?.toString() ?? '') ?? + DateTime.now(), + distribucionId: map['distribucion_id'], + tipoDistribucion: map['tipo_distribucion'], + nombreDistribucion: map['nombre_distribucion'], + ); + } + + // Métodos de utilidad para topología + bool get esMDF => + tipoDistribucion?.toLowerCase() == 'mdf' || + ubicacion?.toLowerCase().contains('mdf') == true || + categoria.toLowerCase().contains('mdf'); + + bool get esIDF => + tipoDistribucion?.toLowerCase() == 'idf' || + ubicacion?.toLowerCase().contains('idf') == true || + categoria.toLowerCase().contains('idf'); + + bool get esSwitch => categoria.toLowerCase().contains('switch'); + + bool get esRouter => + categoria.toLowerCase().contains('router') || + categoria.toLowerCase().contains('firewall'); + + bool get esServidor => + categoria.toLowerCase().contains('servidor') || + categoria.toLowerCase().contains('server'); + + bool get esUPS => categoria.toLowerCase().contains('ups'); + + bool get esRack => categoria.toLowerCase().contains('rack'); + + bool get esPatchPanel => + categoria.toLowerCase().contains('patch') || + categoria.toLowerCase().contains('panel'); + + // Prioridad para ordenamiento en topología + int get prioridadTopologia { + if (esMDF) return 1; + if (esIDF) return 2; + if (esSwitch) return 3; + if (esRouter) return 4; + if (esServidor) return 5; + if (esUPS) return 6; + if (esRack) return 7; + if (esPatchPanel) return 8; + return 9; + } +} + +class ConexionDatos { + final String id; + final String componenteOrigenId; + final String nombreOrigen; + final String componenteDestinoId; + final String nombreDestino; + final String? cableId; + final String? nombreCable; + final String? descripcion; + final bool activo; + + ConexionDatos({ + required this.id, + required this.componenteOrigenId, + required this.nombreOrigen, + required this.componenteDestinoId, + required this.nombreDestino, + this.cableId, + this.nombreCable, + this.descripcion, + required this.activo, + }); + + factory ConexionDatos.fromMap(Map map) { + return ConexionDatos( + id: map['id'] ?? '', + componenteOrigenId: map['componente_origen_id'] ?? '', + nombreOrigen: map['nombre_origen'] ?? '', + componenteDestinoId: map['componente_destino_id'] ?? '', + nombreDestino: map['nombre_destino'] ?? '', + cableId: map['cable_id'], + nombreCable: map['nombre_cable'], + descripcion: map['descripcion'], + activo: map['activo'] ?? false, + ); + } +} + +class ConexionEnergia { + final String id; + final String origenId; + final String nombreOrigen; + final String destinoId; + final String nombreDestino; + final String? cableId; + final String? nombreCable; + final String? descripcion; + final bool activo; + + ConexionEnergia({ + required this.id, + required this.origenId, + required this.nombreOrigen, + required this.destinoId, + required this.nombreDestino, + this.cableId, + this.nombreCable, + this.descripcion, + required this.activo, + }); + + factory ConexionEnergia.fromMap(Map map) { + return ConexionEnergia( + id: map['id'] ?? '', + origenId: map['origen_id'] ?? '', + nombreOrigen: map['nombre_origen'] ?? '', + destinoId: map['destino_id'] ?? '', + nombreDestino: map['nombre_destino'] ?? '', + cableId: map['cable_id'], + nombreCable: map['nombre_cable'], + descripcion: map['descripcion'], + activo: map['activo'] ?? false, + ); + } +} diff --git a/lib/models/nethive/vista_topologia_por_negocio_model.dart b/lib/models/nethive/vista_topologia_por_negocio_model.dart index e650130..85b495c 100644 --- a/lib/models/nethive/vista_topologia_por_negocio_model.dart +++ b/lib/models/nethive/vista_topologia_por_negocio_model.dart @@ -10,6 +10,8 @@ class VistaTopologiaPorNegocio { final String componenteNombre; final String? descripcion; final String categoriaComponente; + final int? rolLogicoId; + final String? rolLogico; final bool enUso; final bool activo; final String? ubicacion; @@ -26,6 +28,8 @@ class VistaTopologiaPorNegocio { required this.componenteNombre, this.descripcion, required this.categoriaComponente, + this.rolLogicoId, + this.rolLogico, required this.enUso, required this.activo, this.ubicacion, @@ -35,29 +39,30 @@ class VistaTopologiaPorNegocio { factory VistaTopologiaPorNegocio.fromMap(Map map) { return VistaTopologiaPorNegocio( - negocioId: map['negocio_id']?.toString() ?? '', - nombreNegocio: map['nombre_negocio']?.toString() ?? '', - distribucionId: map['distribucion_id']?.toString(), - tipoDistribucion: map['tipo_distribucion']?.toString(), - distribucionNombre: map['distribucion_nombre']?.toString(), - componenteId: map['componente_id']?.toString() ?? '', - componenteNombre: map['componente_nombre']?.toString() ?? '', - descripcion: map['descripcion']?.toString(), - categoriaComponente: map['categoria_componente']?.toString() ?? '', + negocioId: map['negocio_id'] ?? '', + nombreNegocio: map['negocio_nombre'] ?? '', + distribucionId: map['distribucion_id'], + tipoDistribucion: map['tipo_distribucion'], + distribucionNombre: map['distribucion_nombre'], + componenteId: map['componente_id'] ?? '', + componenteNombre: map['componente_nombre'] ?? '', + descripcion: map['descripcion'], + categoriaComponente: map['categoria_componente'] ?? '', + rolLogicoId: map['rol_logico_id'], + rolLogico: map['rol_logico'], enUso: map['en_uso'] == true, activo: map['activo'] == true, - ubicacion: map['ubicacion']?.toString(), - imagenUrl: map['imagen_url']?.toString(), + ubicacion: map['ubicacion'], + imagenUrl: map['imagen_url'], fechaRegistro: - DateTime.tryParse(map['fecha_registro']?.toString() ?? '') ?? - DateTime.now(), + DateTime.tryParse(map['fecha_registro'] ?? '') ?? DateTime.now(), ); } Map toMap() { return { 'negocio_id': negocioId, - 'nombre_negocio': nombreNegocio, + 'negocio_nombre': nombreNegocio, 'distribucion_id': distribucionId, 'tipo_distribucion': tipoDistribucion, 'distribucion_nombre': distribucionNombre, @@ -65,6 +70,8 @@ class VistaTopologiaPorNegocio { 'componente_nombre': componenteNombre, 'descripcion': descripcion, 'categoria_componente': categoriaComponente, + 'rol_logico_id': rolLogicoId, + 'rol_logico': rolLogico, 'en_uso': enUso, 'activo': activo, 'ubicacion': ubicacion, @@ -78,20 +85,36 @@ class VistaTopologiaPorNegocio { String toJson() => json.encode(toMap()); - // Método para obtener el tipo de componente principal basado en IDs + bool get esMDF => tipoDistribucion?.toUpperCase() == 'MDF'; + bool get esIDF => tipoDistribucion?.toUpperCase() == 'IDF'; + String get tipoComponentePrincipal { final categoria = categoriaComponente.toLowerCase(); + final nombre = componenteNombre.toLowerCase(); + final desc = descripcion?.toLowerCase() ?? ''; - // Clasificación basada en los nombres de categorías exactos + if (nombre.contains('switch') || desc.contains('switch')) return 'switch'; + if (nombre.contains('router') || + desc.contains('router') || + nombre.contains('firewall') || + desc.contains('firewall') || + nombre.contains('fortigate') || + (nombre.contains('cisco') && + (nombre.contains('asa') || nombre.contains('pix')))) { + return 'router'; + } + if (nombre.contains('servidor') || + nombre.contains('server') || + desc.contains('servidor') || + desc.contains('server')) { + return 'servidor'; + } if (categoria == 'cable') return 'cable'; - if (categoria == 'switch') return 'switch'; if (categoria == 'patch panel') return 'patch_panel'; if (categoria == 'rack') return 'rack'; if (categoria == 'ups') return 'ups'; - if (categoria == 'mdf') return 'mdf'; - if (categoria == 'idf') return 'idf'; + if (categoria.contains('organizador')) return 'organizador'; - // Clasificación por contenido para compatibilidad if (categoria.contains('switch')) return 'switch'; if (categoria.contains('router') || categoria.contains('firewall')) return 'router'; @@ -102,32 +125,13 @@ class VistaTopologiaPorNegocio { return 'patch_panel'; if (categoria.contains('rack')) return 'rack'; if (categoria.contains('ups')) return 'ups'; - if (categoria.contains('organizador')) return 'organizador'; return 'otro'; } - // Método mejorado para determinar si es MDF - bool get esMDF { - final categoria = categoriaComponente.toLowerCase(); - return categoria == 'mdf' || - tipoDistribucion?.toUpperCase() == 'MDF' || - ubicacion?.toLowerCase().contains('mdf') == true; - } - - // Método mejorado para determinar si es IDF - bool get esIDF { - final categoria = categoriaComponente.toLowerCase(); - return categoria == 'idf' || - tipoDistribucion?.toUpperCase() == 'IDF' || - ubicacion?.toLowerCase().contains('idf') == true; - } - - // Método para obtener el nivel de prioridad del componente (para ordenamiento en topología) int get prioridadTopologia { - if (esMDF) return 1; // Máxima prioridad para MDF - if (esIDF) return 2; // Segunda prioridad para IDF - + if (esMDF) return 1; + if (esIDF) return 2; switch (tipoComponentePrincipal) { case 'router': return 3; @@ -150,32 +154,26 @@ class VistaTopologiaPorNegocio { } } - // Método para determinar el color del componente en el diagrama String getColorForDiagram() { - if (esMDF) return '#2196F3'; // Azul para MDF - if (esIDF) { - return enUso - ? '#4CAF50' - : '#FF9800'; // Verde si está en uso, naranja si no - } - + if (esMDF) return '#2196F3'; + if (esIDF) return enUso ? '#4CAF50' : '#FF9800'; switch (tipoComponentePrincipal) { case 'router': - return '#FF5722'; // Naranja rojizo + return '#FF5722'; case 'switch': - return '#9C27B0'; // Morado + return '#9C27B0'; case 'servidor': - return '#E91E63'; // Rosa + return '#E91E63'; case 'patch_panel': - return '#607D8B'; // Azul gris + return '#607D8B'; case 'rack': - return '#795548'; // Marrón + return '#795548'; case 'ups': - return '#FFC107'; // Ámbar + return '#FFC107'; case 'cable': - return '#4CAF50'; // Verde + return '#4CAF50'; case 'organizador': - return '#9E9E9E'; // Gris + return '#9E9E9E'; default: return activo ? '#2196F3' : '#757575'; } diff --git a/lib/pages/infrastructure/pages/topologia_page.dart b/lib/pages/infrastructure/pages/topologia_page.dart index d2a12e5..5f3c8b4 100644 --- a/lib/pages/infrastructure/pages/topologia_page.dart +++ b/lib/pages/infrastructure/pages/topologia_page.dart @@ -4,8 +4,7 @@ import 'package:flutter_flow_chart/flutter_flow_chart.dart'; 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/vista_topologia_por_negocio_model.dart'; -import 'package:nethive_neo/models/nethive/vista_conexiones_por_cables_model.dart'; +import 'package:nethive_neo/models/nethive/topologia_completa_model.dart'; class TopologiaPage extends StatefulWidget { const TopologiaPage({Key? key}) : super(key: key); @@ -25,15 +24,8 @@ class _TopologiaPageState extends State // Dashboard para el FlowChart late Dashboard dashboard; - // Elementos para referencias - late FlowElement mdfElement; - late FlowElement idf1Element; - late FlowElement idf2Element; - late FlowElement switch1Element; - late FlowElement switch2Element; - late FlowElement switch3Element; - late FlowElement switch4Element; - late FlowElement serverElement; + // Mapas para elementos y conexiones + Map elementosMap = {}; @override void initState() { @@ -55,7 +47,7 @@ class _TopologiaPageState extends State // Cargar datos después de que el widget esté construido WidgetsBinding.instance.addPostFrameCallback((_) { - _loadRealTopologyData(); + _loadTopologyData(); }); } @@ -66,310 +58,6 @@ class _TopologiaPageState extends State ); } - void _buildNetworkTopology() { - dashboard.removeAllElements(); - - // MDF Principal - mdfElement = FlowElement( - position: const Offset(400, 100), - size: const Size(160, 120), - text: 'MDF\nPrincipal', - textColor: Colors.white, - textSize: 14, - textIsBold: true, - kind: ElementKind.rectangle, - backgroundColor: const Color(0xFF2196F3), - borderColor: const Color(0xFF1976D2), - borderThickness: 3, - elevation: 8, - data: { - 'type': 'MDF', - 'name': 'MDF Principal', - 'status': 'active', - 'ports': '2/48', - 'description': - 'Main Distribution Frame\nSwitch Principal 48p\nPatch Panel 48p\nUPS Respaldo' - }, - handlers: [ - Handler.bottomCenter, - Handler.leftCenter, - Handler.rightCenter, - ], - ); - - // IDF 1 - idf1Element = FlowElement( - position: const Offset(200, 300), - size: const Size(140, 100), - text: 'IDF 1\nPiso 1', - textColor: Colors.white, - textSize: 12, - textIsBold: true, - kind: ElementKind.rectangle, - backgroundColor: const Color(0xFF4CAF50), - borderColor: const Color(0xFF388E3C), - borderThickness: 2, - elevation: 6, - data: { - 'type': 'IDF', - 'name': 'IDF Piso 1', - 'status': 'active', - 'ports': '32/48', - 'description': - 'Intermediate Distribution Frame\nSwitch 48p\nPatch Panel\nUPS' - }, - handlers: [ - Handler.topCenter, - Handler.bottomCenter, - Handler.leftCenter, - Handler.rightCenter, - ], - ); - - // IDF 2 - idf2Element = FlowElement( - position: const Offset(600, 300), - size: const Size(140, 100), - text: 'IDF 2\nPiso 2', - textColor: Colors.white, - textSize: 12, - textIsBold: true, - kind: ElementKind.rectangle, - backgroundColor: const Color(0xFFFF9800), - borderColor: const Color(0xFFF57C00), - borderThickness: 2, - elevation: 6, - data: { - 'type': 'IDF', - 'name': 'IDF Piso 2', - 'status': 'warning', - 'ports': '45/48', - 'description': - 'Intermediate Distribution Frame\nSwitch 48p\nPatch Panel\nUPS\n⚠️ Alta utilización' - }, - handlers: [ - Handler.topCenter, - Handler.bottomCenter, - Handler.leftCenter, - Handler.rightCenter, - ], - ); - - // Switches de Acceso - switch1Element = FlowElement( - position: const Offset(125, 500), - size: const Size(120, 80), - text: 'Switch\nAcceso A1', - textColor: Colors.white, - textSize: 10, - textIsBold: true, - kind: ElementKind.rectangle, - backgroundColor: const Color(0xFF9C27B0), - borderColor: const Color(0xFF7B1FA2), - borderThickness: 2, - elevation: 4, - data: { - 'type': 'AccessSwitch', - 'name': 'Switch Acceso A1', - 'status': 'active', - 'ports': '16/24', - 'description': 'Switch de Acceso\n24 puertos\nEn línea' - }, - handlers: [ - Handler.topCenter, - ], - ); - - switch2Element = FlowElement( - position: const Offset(275, 500), - size: const Size(120, 80), - text: 'Switch\nAcceso A2', - textColor: Colors.white, - textSize: 10, - textIsBold: true, - kind: ElementKind.rectangle, - backgroundColor: const Color(0xFF9C27B0), - borderColor: const Color(0xFF7B1FA2), - borderThickness: 2, - elevation: 4, - data: { - 'type': 'AccessSwitch', - 'name': 'Switch Acceso A2', - 'status': 'active', - 'ports': '20/24', - 'description': 'Switch de Acceso\n24 puertos\nEn línea' - }, - handlers: [ - Handler.topCenter, - ], - ); - - switch3Element = FlowElement( - position: const Offset(525, 500), - size: const Size(120, 80), - text: 'Switch\nAcceso B1', - textColor: Colors.white, - textSize: 10, - textIsBold: true, - kind: ElementKind.rectangle, - backgroundColor: const Color(0xFF9C27B0), - borderColor: const Color(0xFF7B1FA2), - borderThickness: 2, - elevation: 4, - data: { - 'type': 'AccessSwitch', - 'name': 'Switch Acceso B1', - 'status': 'active', - 'ports': '18/24', - 'description': 'Switch de Acceso\n24 puertos\nEn línea' - }, - handlers: [ - Handler.topCenter, - ], - ); - - switch4Element = FlowElement( - position: const Offset(675, 500), - size: const Size(120, 80), - text: 'Switch\nAcceso B2', - textColor: Colors.white, - textSize: 10, - textIsBold: false, - kind: ElementKind.rectangle, - backgroundColor: const Color(0xFF757575), - borderColor: const Color(0xFF424242), - borderThickness: 2, - elevation: 2, - data: { - 'type': 'AccessSwitch', - 'name': 'Switch Acceso B2', - 'status': 'disconnected', - 'ports': '0/24', - 'description': 'Switch de Acceso\n24 puertos\n🔴 Desconectado' - }, - handlers: [ - Handler.topCenter, - ], - ); - - // Servidor Principal - serverElement = FlowElement( - position: const Offset(400, 650), - size: const Size(150, 90), - text: 'Servidor\nPrincipal', - textColor: Colors.white, - textSize: 12, - textIsBold: true, - kind: ElementKind.rectangle, - backgroundColor: const Color(0xFFE91E63), - borderColor: const Color(0xFFC2185B), - borderThickness: 3, - elevation: 6, - data: { - 'type': 'Server', - 'name': 'Servidor Principal', - 'status': 'active', - 'description': - 'Servidor Principal\nWindows Server 2022\nRAM: 32GB\nStorage: 2TB SSD' - }, - handlers: [ - Handler.topCenter, - ], - ); - - // Agregar elementos al dashboard - dashboard.addElement(mdfElement); - dashboard.addElement(idf1Element); - dashboard.addElement(idf2Element); - dashboard.addElement(switch1Element); - dashboard.addElement(switch2Element); - dashboard.addElement(switch3Element); - dashboard.addElement(switch4Element); - dashboard.addElement(serverElement); - - // Crear conexiones - _createConnections(); - } - - void _createConnections() { - // MDF -> IDF1 (Fibra) - mdfElement.next = [ - ConnectionParams( - destElementId: idf1Element.id, - arrowParams: ArrowParams( - color: Colors.cyan, - thickness: 4, - ), - ), - ]; - - // MDF -> IDF2 (Fibra) - mdfElement.next!.add( - ConnectionParams( - destElementId: idf2Element.id, - arrowParams: ArrowParams( - color: Colors.cyan, - thickness: 4, - ), - ), - ); - - // IDF1 -> Switch A1 (UTP) - idf1Element.next = [ - ConnectionParams( - destElementId: switch1Element.id, - arrowParams: ArrowParams( - color: Colors.yellow, - thickness: 3, - ), - ), - ]; - - // IDF1 -> Switch A2 (UTP) - idf1Element.next!.add( - ConnectionParams( - destElementId: switch2Element.id, - arrowParams: ArrowParams( - color: Colors.yellow, - thickness: 3, - ), - ), - ); - - // IDF2 -> Switch B1 (UTP) - idf2Element.next = [ - ConnectionParams( - destElementId: switch3Element.id, - arrowParams: ArrowParams( - color: Colors.yellow, - thickness: 3, - ), - ), - ]; - - // IDF2 -> Switch B2 (UTP - Desconectado) - idf2Element.next!.add( - ConnectionParams( - destElementId: switch4Element.id, - arrowParams: ArrowParams( - color: Colors.grey, - thickness: 2, - ), - ), - ); - - // MDF -> Servidor (Dedicado) - mdfElement.next!.add( - ConnectionParams( - destElementId: serverElement.id, - arrowParams: ArrowParams( - color: Colors.purple, - thickness: 5, - ), - ), - ); - } - @override void dispose() { _animationController.dispose(); @@ -384,28 +72,39 @@ class _TopologiaPageState extends State opacity: _fadeAnimation, child: Consumer( builder: (context, componentesProvider, child) { - if (_isLoading) { + if (componentesProvider.isLoadingTopologia || _isLoading) { return _buildLoadingView(); } + // Verificar si hay un negocio seleccionado + if (componentesProvider.negocioSeleccionadoId == null) { + return _buildNoBusinessSelectedView(); + } + + // Verificar si hay componentes + if (componentesProvider.componentesTopologia.isEmpty) { + return _buildNoComponentsView(); + } + return Container( padding: EdgeInsets.all(isMediumScreen ? 24 : 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header profesional - _buildProfessionalHeader(), + _buildProfessionalHeader(componentesProvider), const SizedBox(height: 24), // Controles avanzados if (isMediumScreen) ...[ - _buildAdvancedControls(), + _buildAdvancedControls(componentesProvider), const SizedBox(height: 24), ], // Vista principal profesional Expanded( - child: _buildProfessionalTopologyView(isMediumScreen), + child: _buildProfessionalTopologyView( + isMediumScreen, componentesProvider), ), ], ), @@ -435,7 +134,7 @@ class _TopologiaPageState extends State ).animate().fadeIn(delay: 400.ms), const SizedBox(height: 8), Text( - 'Construyendo infraestructura profesional', + 'Construyendo infraestructura desde la base de datos', style: TextStyle( color: AppTheme.of(context).secondaryText, fontSize: 14, @@ -446,7 +145,97 @@ class _TopologiaPageState extends State ); } - Widget _buildProfessionalHeader() { + Widget _buildNoBusinessSelectedView() { + return Center( + 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, + size: 80, + color: Colors.white, + ), + const SizedBox(height: 20), + Text( + 'Selecciona un Negocio', + style: const TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 12), + Text( + 'Debe seleccionar un negocio desde la gestión\nde empresas para visualizar su topología', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white.withOpacity(0.9), + fontSize: 16, + ), + ), + ], + ), + ), + ); + } + + Widget _buildNoComponentsView() { + return Center( + child: Container( + padding: const EdgeInsets.all(40), + 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.device_hub_outlined, + size: 80, + color: AppTheme.of(context).primaryColor, + ), + const SizedBox(height: 20), + Text( + 'Sin Componentes', + style: TextStyle( + color: AppTheme.of(context).primaryText, + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 12), + Text( + 'Este negocio no tiene componentes\nregistrados en la infraestructura', + textAlign: TextAlign.center, + style: TextStyle( + color: AppTheme.of(context).secondaryText, + fontSize: 16, + ), + ), + ], + ), + ), + ); + } + + Widget _buildProfessionalHeader(ComponentesProvider provider) { return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( @@ -473,57 +262,59 @@ class _TopologiaPageState extends State color: Colors.white, size: 24, ), - ) - .animate() - .scale(duration: 600.ms) - .then(delay: 200.ms) - .rotate(begin: 0, end: 0.1) - .then() - .rotate(begin: 0.1, end: 0), + ).animate().scale(duration: 600.ms), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - 'Topología Interactiva de Red', - style: TextStyle( + Text( + 'Topología de Red - ${provider.negocioSeleccionadoNombre ?? "Negocio"}', + style: const TextStyle( color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold, ), - ).animate().fadeIn(delay: 300.ms).slideX(begin: -0.3, end: 0), + ).animate().fadeIn(delay: 300.ms), Text( - 'Diagrama profesional con flutter_flow_chart', + '${provider.componentesTopologia.length} componentes • ${provider.conexionesDatos.length} conexiones de datos', style: TextStyle( color: Colors.white.withOpacity(0.9), fontSize: 14, ), - ).animate().fadeIn(delay: 500.ms).slideX(begin: -0.3, end: 0), + ).animate().fadeIn(delay: 500.ms), ], ), ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(12), - ), - child: const Text( - 'FLOW', - style: TextStyle( - color: Colors.white, - fontSize: 12, - fontWeight: FontWeight.w600, + if (provider.problemasTopologia.isNotEmpty) + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.orange.withOpacity(0.3), + borderRadius: BorderRadius.circular(12), ), - ), - ).animate().fadeIn(delay: 700.ms).scale(), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.warning, color: Colors.white, size: 16), + const SizedBox(width: 8), + Text( + '${provider.problemasTopologia.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 _buildAdvancedControls() { + Widget _buildAdvancedControls(ComponentesProvider provider) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( @@ -556,32 +347,68 @@ class _TopologiaPageState extends State ), ), + // Información de componentes + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: AppTheme.of(context).primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Resumen:', + style: TextStyle( + color: AppTheme.of(context).primaryText, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 4), + Row( + children: [ + _buildStatChip('MDF', provider.getComponentesMDF().length, + Colors.blue), + const SizedBox(width: 8), + _buildStatChip('IDF', provider.getComponentesIDF().length, + Colors.green), + const SizedBox(width: 8), + _buildStatChip('Switch', + provider.getComponentesSwitch().length, Colors.purple), + ], + ), + ], + ), + ), + + const SizedBox(width: 16), + // Controles de la topología Row( children: [ - Text( - 'Controles:', - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(width: 12), IconButton( onPressed: () { - setState(() { - _buildNetworkTopology(); - }); + _refreshTopology(provider); }, icon: const Icon(Icons.refresh), tooltip: 'Actualizar topología', + style: IconButton.styleFrom( + backgroundColor: + AppTheme.of(context).primaryColor.withOpacity(0.1), + ), ), + const SizedBox(width: 8), IconButton( onPressed: () { dashboard.setZoomFactor(1.0); }, icon: const Icon(Icons.center_focus_strong), tooltip: 'Centrar vista', + style: IconButton.styleFrom( + backgroundColor: + AppTheme.of(context).primaryColor.withOpacity(0.1), + ), ), ], ), @@ -590,6 +417,24 @@ class _TopologiaPageState extends State ).animate().fadeIn(delay: 200.ms).slideY(begin: -0.2, end: 0); } + Widget _buildStatChip(String label, int count, Color color) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + '$label: $count', + style: TextStyle( + color: color, + fontSize: 10, + fontWeight: FontWeight.w600, + ), + ), + ); + } + Widget _buildViewButton(String value, String label, IconData icon) { final isSelected = _selectedView == value; return GestureDetector( @@ -633,7 +478,8 @@ class _TopologiaPageState extends State ); } - Widget _buildProfessionalTopologyView(bool isMediumScreen) { + Widget _buildProfessionalTopologyView( + bool isMediumScreen, ComponentesProvider provider) { return Container( decoration: BoxDecoration( color: const Color(0xFF0D1117), // Fondo oscuro profesional tipo GitHub @@ -655,11 +501,11 @@ class _TopologiaPageState extends State children: [ // Vista según selección if (_selectedView == 'network') - _buildInteractiveFlowChart() + _buildInteractiveFlowChart(provider) else if (_selectedView == 'rack') - _buildRackView(isMediumScreen) + _buildRackView(isMediumScreen, provider) else if (_selectedView == 'floor') - _buildFloorPlanView(isMediumScreen), + _buildFloorPlanView(isMediumScreen, provider), // Leyenda profesional if (isMediumScreen && _selectedView == 'network') @@ -674,7 +520,15 @@ class _TopologiaPageState extends State Positioned( top: 16, left: 16, - child: _buildInfoPanel(), + child: _buildInfoPanel(provider), + ), + + // Panel de problemas si existen + if (provider.problemasTopologia.isNotEmpty) + Positioned( + bottom: 16, + left: 16, + child: _buildProblemasPanel(provider), ), ], ), @@ -682,30 +536,383 @@ class _TopologiaPageState extends State ); } - Widget _buildInteractiveFlowChart() { - return FlowChart( - dashboard: dashboard, - onElementPressed: (context, position, element) { - _showElementDetails(element); + Widget _buildInteractiveFlowChart(ComponentesProvider provider) { + return FutureBuilder( + future: _buildNetworkTopologyFromData(provider), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center( + child: CircularProgressIndicator(color: Colors.white), + ); + } + + return FlowChart( + dashboard: dashboard, + onElementPressed: (context, position, element) { + _showElementDetails(element, provider); + }, + onElementLongPressed: (context, position, element) { + _showElementContextMenu(context, position, element, provider); + }, + onNewConnection: (source, target) { + _handleNewConnection(source, target, provider); + }, + onDashboardTapped: (context, position) { + // Limpiar selecciones + }, + ) + .animate() + .fadeIn(duration: 800.ms) + .scale(begin: const Offset(0.95, 0.95)); }, - onElementLongPressed: (context, position, element) { - _showElementContextMenu(context, position, element); - }, - onNewConnection: (source, target) { - _handleNewConnection(source, target); - }, - onDashboardTapped: (context, position) { - // Limpiar selecciones - }, - ).animate().fadeIn(duration: 800.ms).scale(begin: const Offset(0.95, 0.95)); + ); } - void _showElementDetails(FlowElement element) { + Future _buildNetworkTopologyFromData( + ComponentesProvider provider) async { + dashboard.removeAllElements(); + elementosMap.clear(); + + // Obtener componentes organizados por tipo + final mdfComponents = provider.getComponentesMDF(); + final idfComponents = provider.getComponentesIDF(); + final switchComponents = provider.getComponentesSwitch(); + final routerComponents = provider.getComponentesRouter(); + final serverComponents = provider.getComponentesServidor(); + + print('Construyendo topología con datos reales:'); + print('- MDF: ${mdfComponents.length}'); + print('- IDF: ${idfComponents.length}'); + print('- Switches: ${switchComponents.length}'); + print('- Routers: ${routerComponents.length}'); + print('- Servidores: ${serverComponents.length}'); + + double currentX = 100; + double currentY = 100; + const double espacioX = 220; + const double espacioY = 180; + + // 1. Crear elementos MDF + if (mdfComponents.isNotEmpty) { + double mdfX = currentX + espacioX * 2; + for (var mdfComp in mdfComponents) { + final mdfElement = _createMDFElement(mdfComp, Offset(mdfX, currentY)); + dashboard.addElement(mdfElement); + elementosMap[mdfComp.id] = mdfElement; + mdfX += espacioX * 0.8; + } + currentY += espacioY; + } + + // 2. Crear elementos IDF + if (idfComponents.isNotEmpty) { + double idfX = currentX; + for (var idfComp in idfComponents) { + final idfElement = _createIDFElement(idfComp, Offset(idfX, currentY)); + dashboard.addElement(idfElement); + elementosMap[idfComp.id] = idfElement; + idfX += espacioX; + } + currentY += espacioY; + } + + // 3. Crear routers + if (routerComponents.isNotEmpty) { + double routerX = currentX; + for (var router in routerComponents) { + final routerElement = + _createRouterElement(router, Offset(routerX, currentY)); + dashboard.addElement(routerElement); + elementosMap[router.id] = routerElement; + routerX += espacioX * 0.9; + } + currentY += espacioY; + } + + // 4. Crear switches + if (switchComponents.isNotEmpty) { + double switchX = currentX; + for (var switchComp in switchComponents) { + final switchElement = + _createSwitchElement(switchComp, Offset(switchX, currentY)); + dashboard.addElement(switchElement); + elementosMap[switchComp.id] = switchElement; + switchX += espacioX * 0.8; + } + currentY += espacioY; + } + + // 5. Crear servidores + if (serverComponents.isNotEmpty) { + double serverX = currentX + espacioX; + for (var servidor in serverComponents) { + final serverElement = + _createServerElement(servidor, Offset(serverX, currentY)); + dashboard.addElement(serverElement); + elementosMap[servidor.id] = serverElement; + serverX += espacioX * 0.8; + } + } + + // 6. Crear conexiones basadas en datos reales + _createConnections(provider); + + print('Elementos creados: ${elementosMap.length}'); + print( + 'Conexiones de datos disponibles: ${provider.conexionesDatos.length}'); + } + + FlowElement _createMDFElement( + ComponenteTopologia component, Offset position) { + return FlowElement( + position: position, + size: const Size(180, 140), + text: 'MDF\n${component.nombre}', + textColor: Colors.white, + textSize: 14, + textIsBold: true, + kind: ElementKind.rectangle, + backgroundColor: + component.activo ? const Color(0xFF2196F3) : const Color(0xFF757575), + borderColor: + component.activo ? const Color(0xFF1976D2) : const Color(0xFF424242), + borderThickness: 3, + elevation: component.activo ? 8 : 4, + data: _buildElementData(component, 'MDF'), + handlers: [ + Handler.bottomCenter, + Handler.leftCenter, + Handler.rightCenter, + ], + ); + } + + FlowElement _createIDFElement( + ComponenteTopologia component, Offset position) { + return FlowElement( + position: position, + size: const Size(160, 120), + text: 'IDF\n${component.nombre}', + textColor: Colors.white, + textSize: 12, + textIsBold: true, + kind: ElementKind.rectangle, + backgroundColor: component.activo + ? (component.enUso + ? const Color(0xFF4CAF50) + : const Color(0xFFFF9800)) + : const Color(0xFF757575), + borderColor: component.activo + ? (component.enUso + ? const Color(0xFF388E3C) + : const Color(0xFFF57C00)) + : const Color(0xFF424242), + borderThickness: 2, + elevation: component.activo ? 6 : 2, + data: _buildElementData(component, 'IDF'), + handlers: [ + Handler.topCenter, + Handler.bottomCenter, + Handler.leftCenter, + Handler.rightCenter, + ], + ); + } + + FlowElement _createSwitchElement( + ComponenteTopologia component, Offset position) { + return FlowElement( + position: position, + size: const Size(140, 100), + text: 'Switch\n${component.nombre}', + textColor: Colors.white, + textSize: 10, + textIsBold: true, + kind: ElementKind.rectangle, + backgroundColor: + component.activo ? const Color(0xFF9C27B0) : const Color(0xFF757575), + borderColor: + component.activo ? const Color(0xFF7B1FA2) : const Color(0xFF424242), + borderThickness: 2, + elevation: component.activo ? 4 : 2, + data: _buildElementData(component, 'Switch'), + handlers: [ + Handler.topCenter, + Handler.bottomCenter, + ], + ); + } + + FlowElement _createRouterElement( + ComponenteTopologia component, Offset position) { + return FlowElement( + position: position, + size: const Size(160, 100), + text: 'Router\n${component.nombre}', + textColor: Colors.white, + textSize: 11, + textIsBold: true, + kind: ElementKind.rectangle, + backgroundColor: + component.activo ? const Color(0xFFFF5722) : const Color(0xFF757575), + borderColor: + component.activo ? const Color(0xFFE64A19) : const Color(0xFF424242), + borderThickness: 3, + elevation: component.activo ? 6 : 2, + data: _buildElementData(component, 'Router'), + handlers: [ + Handler.topCenter, + Handler.bottomCenter, + Handler.leftCenter, + Handler.rightCenter, + ], + ); + } + + FlowElement _createServerElement( + ComponenteTopologia component, Offset position) { + return FlowElement( + position: position, + size: const Size(150, 100), + text: 'Servidor\n${component.nombre}', + textColor: Colors.white, + textSize: 11, + textIsBold: true, + kind: ElementKind.rectangle, + backgroundColor: + component.activo ? const Color(0xFFE91E63) : const Color(0xFF757575), + borderColor: + component.activo ? const Color(0xFFC2185B) : const Color(0xFF424242), + borderThickness: 3, + elevation: component.activo ? 6 : 2, + data: _buildElementData(component, 'Server'), + handlers: [ + Handler.topCenter, + Handler.leftCenter, + Handler.rightCenter, + ], + ); + } + + Map _buildElementData( + ComponenteTopologia component, String displayType) { + return { + 'type': displayType, + 'componenteId': component.id, + 'name': component.nombre, + 'categoria': component.categoria, + 'status': component.activo + ? (component.enUso ? 'active' : 'warning') + : 'disconnected', + 'description': component.descripcion ?? 'Sin descripción', + 'ubicacion': component.ubicacion ?? 'Sin ubicación', + 'distribucion': component.nombreDistribucion ?? 'Sin distribución', + 'tipoDistribucion': component.tipoDistribucion, + 'enUso': component.enUso, + 'fechaRegistro': component.fechaRegistro.toString().split(' ')[0], + }; + } + + void _createConnections(ComponentesProvider provider) { + // Crear conexiones basadas en los datos reales + for (var conexion in provider.conexionesDatos) { + if (!conexion.activo) continue; + + final sourceElement = elementosMap[conexion.componenteOrigenId]; + final targetElement = elementosMap[conexion.componenteDestinoId]; + + if (sourceElement != null && targetElement != null) { + // Determinar color y grosor basado en el tipo de conexión + Color connectionColor = _getConnectionColor(conexion, provider); + double thickness = _getConnectionThickness(conexion, provider); + + final connectionParams = ConnectionParams( + destElementId: targetElement.id, + arrowParams: ArrowParams( + color: connectionColor, + thickness: thickness, + ), + ); + + sourceElement.next = [...sourceElement.next ?? [], connectionParams]; + } + } + + // También crear conexiones de energía si es necesario + for (var conexionEnergia in provider.conexionesEnergia) { + if (!conexionEnergia.activo) continue; + + final sourceElement = elementosMap[conexionEnergia.origenId]; + final targetElement = elementosMap[conexionEnergia.destinoId]; + + if (sourceElement != null && targetElement != null) { + final connectionParams = ConnectionParams( + destElementId: targetElement.id, + arrowParams: ArrowParams( + color: Colors.red.withOpacity(0.7), + thickness: 2, + ), + ); + + sourceElement.next = [...sourceElement.next ?? [], connectionParams]; + } + } + } + + Color _getConnectionColor( + ConexionDatos conexion, ComponentesProvider provider) { + // Determinar color basado en el tipo de cable o componentes conectados + if (conexion.nombreCable != null) { + final cableName = conexion.nombreCable!.toLowerCase(); + if (cableName.contains('fibra')) return Colors.cyan; + if (cableName.contains('utp')) return Colors.yellow; + if (cableName.contains('coaxial')) return Colors.orange; + } + + // Color por defecto basado en los componentes + final sourceComponent = + provider.getComponenteTopologiaById(conexion.componenteOrigenId); + final targetComponent = + provider.getComponenteTopologiaById(conexion.componenteDestinoId); + + if (sourceComponent?.esMDF == true || targetComponent?.esMDF == true) { + return Colors.cyan; // Conexiones principales + } + if (sourceComponent?.esIDF == true || targetComponent?.esIDF == true) { + return Colors.yellow; // Conexiones intermedias + } + + return Colors.green; // Conexiones generales + } + + double _getConnectionThickness( + ConexionDatos conexion, ComponentesProvider provider) { + final sourceComponent = + provider.getComponenteTopologiaById(conexion.componenteOrigenId); + final targetComponent = + provider.getComponenteTopologiaById(conexion.componenteDestinoId); + + if (sourceComponent?.esMDF == true || targetComponent?.esMDF == true) { + return 4; // Conexiones principales más gruesas + } + if (sourceComponent?.esIDF == true || targetComponent?.esIDF == true) { + return 3; // Conexiones intermedias + } + + return 2; // Conexiones estándar + } + + void _showElementDetails(FlowElement element, ComponentesProvider provider) { final data = element.data as Map; + final componenteId = data['componenteId'] as String; + final component = provider.getComponenteTopologiaById(componenteId); + + if (component == null) return; showDialog( context: context, builder: (context) => AlertDialog( + backgroundColor: AppTheme.of(context).primaryBackground, title: Row( children: [ Icon(_getIconForType(data['type']), @@ -716,24 +923,31 @@ class _TopologiaPageState extends State ), content: Container( width: double.maxFinite, - constraints: const BoxConstraints(maxHeight: 400), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Tipo: ${data['type']}'), - const SizedBox(height: 8), - Text('Estado: ${_getStatusText(data['status'])}'), - if (data['ports'] != null) ...[ + constraints: const BoxConstraints(maxHeight: 500), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildDetailRow('Tipo', data['type']), + _buildDetailRow('Categoría', component.categoria), + _buildDetailRow('Estado', _getStatusText(data['status'])), + _buildDetailRow('En Uso', component.enUso ? 'Sí' : 'No'), + _buildDetailRow( + 'Ubicación', component.ubicacion ?? 'Sin especificar'), + if (component.tipoDistribucion != null) + _buildDetailRow('Distribución', + '${component.tipoDistribucion} - ${component.nombreDistribucion}'), + _buildDetailRow('Fecha Registro', + component.fechaRegistro.toString().split(' ')[0]), + const SizedBox(height: 16), + const Text('Descripción:', + style: TextStyle(fontWeight: FontWeight.bold)), const SizedBox(height: 8), - Text('Puertos: ${data['ports']}'), + Text(component.descripcion ?? 'Sin descripción'), + const SizedBox(height: 16), + _buildConnectionsInfo(component, provider), ], - const SizedBox(height: 12), - const Text('Descripción:', - style: TextStyle(fontWeight: FontWeight.bold)), - const SizedBox(height: 8), - Text(data['description']), - ], + ), ), ), actions: [ @@ -746,13 +960,69 @@ class _TopologiaPageState extends State ); } - void _showElementContextMenu( - BuildContext context, Offset position, FlowElement element) { - // Implementar menú contextual + Widget _buildDetailRow(String label, String value) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 120, + child: Text( + '$label:', + style: const TextStyle(fontWeight: FontWeight.w600), + ), + ), + Expanded(child: Text(value)), + ], + ), + ); } - void _handleNewConnection(FlowElement source, FlowElement target) { - // Mostrar diálogo para configurar nueva conexión + Widget _buildConnectionsInfo( + ComponenteTopologia component, ComponentesProvider provider) { + final conexiones = provider.getConexionesPorComponente(component.id); + final conexionesEnergia = + provider.getConexionesEnergiaPorComponente(component.id); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Conexiones:', + style: TextStyle(fontWeight: FontWeight.bold)), + const SizedBox(height: 8), + if (conexiones.isEmpty && conexionesEnergia.isEmpty) + const Text('Sin conexiones registradas') + else ...[ + if (conexiones.isNotEmpty) ...[ + const Text('Datos:', style: TextStyle(fontWeight: FontWeight.w500)), + ...conexiones.map((c) => Padding( + padding: const EdgeInsets.only(left: 16, top: 4), + child: Text('• ${c.nombreOrigen} ↔ ${c.nombreDestino}'), + )), + ], + if (conexionesEnergia.isNotEmpty) ...[ + const SizedBox(height: 8), + const Text('Energía:', + style: TextStyle(fontWeight: FontWeight.w500)), + ...conexionesEnergia.map((c) => Padding( + padding: const EdgeInsets.only(left: 16, top: 4), + child: Text('• ${c.nombreOrigen} → ${c.nombreDestino}'), + )), + ], + ], + ], + ); + } + + void _showElementContextMenu(BuildContext context, Offset position, + FlowElement element, ComponentesProvider provider) { + // TODO: Implementar menú contextual con opciones reales + } + + void _handleNewConnection( + FlowElement source, FlowElement target, ComponentesProvider provider) { + // TODO: Implementar creación de nueva conexión en la base de datos showDialog( context: context, builder: (context) => AlertDialog( @@ -763,31 +1033,13 @@ class _TopologiaPageState extends State Text('Conectar desde: ${(source.data as Map)['name']}'), Text('Hacia: ${(target.data as Map)['name']}'), const SizedBox(height: 16), - const Text('Seleccione el tipo de conexión:'), - // Aquí podrías agregar controles para seleccionar tipo de cable + const Text('Funcionalidad próximamente...'), ], ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), - child: const Text('Cancelar'), - ), - ElevatedButton( - onPressed: () { - // Crear la conexión manualmente - final newConnection = ConnectionParams( - destElementId: target.id, - arrowParams: ArrowParams( - color: Colors.green, - thickness: 3, - ), - ); - - source.next = [...source.next ?? [], newConnection]; - Navigator.of(context).pop(); - setState(() {}); - }, - child: const Text('Conectar'), + child: const Text('Cerrar'), ), ], ), @@ -820,13 +1072,15 @@ class _TopologiaPageState extends State _buildLegendItem( const Color(0xFFFF9800), 'IDF Advertencia', Icons.hub), _buildLegendItem( - const Color(0xFF9C27B0), 'Switch Acceso', Icons.network_check), + const Color(0xFF9C27B0), 'Switch', Icons.network_check), + _buildLegendItem(const Color(0xFFFF5722), 'Router', Icons.router), _buildLegendItem(const Color(0xFFE91E63), 'Servidor', Icons.dns), const SizedBox(height: 6), _buildLegendItem(Colors.cyan, 'Fibra Óptica', Icons.cable), _buildLegendItem(Colors.yellow, 'Cable UTP', Icons.cable), - _buildLegendItem(Colors.purple, 'Conexión Dedicada', Icons.cable), - _buildLegendItem(Colors.grey, 'Desconectado', Icons.cable), + _buildLegendItem(Colors.green, 'Conexión General', Icons.cable), + _buildLegendItem(Colors.red, 'Alimentación', Icons.power), + _buildLegendItem(Colors.grey, 'Inactivo', Icons.clear), ], ), ).animate().fadeIn(delay: 1000.ms).slideX(begin: 0.3); @@ -852,7 +1106,7 @@ class _TopologiaPageState extends State ); } - Widget _buildInfoPanel() { + Widget _buildInfoPanel(ComponentesProvider provider) { return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( @@ -860,11 +1114,11 @@ class _TopologiaPageState extends State borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.white.withOpacity(0.2)), ), - child: const Column( + child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( + const Text( 'Información', style: TextStyle( color: Colors.white, @@ -872,47 +1126,136 @@ class _TopologiaPageState extends State fontWeight: FontWeight.bold, ), ), - SizedBox(height: 8), - Text( + const SizedBox(height: 8), + const Text( '• Arrastra los nodos para reposicionar', style: TextStyle(color: Colors.white70, fontSize: 10), ), - Text( + const Text( '• Haz clic en un nodo para ver detalles', style: TextStyle(color: Colors.white70, fontSize: 10), ), - Text( - '• Arrastra desde los puntos de conexión', - style: TextStyle(color: Colors.white70, fontSize: 10), - ), - Text( + const Text( '• Usa zoom con scroll del mouse', style: TextStyle(color: Colors.white70, fontSize: 10), ), + const SizedBox(height: 8), + Text( + 'Datos desde: ${provider.negocioSeleccionadoNombre}', + style: const TextStyle(color: Colors.cyan, fontSize: 9), + ), ], ), ).animate().fadeIn(delay: 1200.ms).slideX(begin: -0.3); } - Widget _buildRackView(bool isMediumScreen) { + Widget _buildProblemasPanel(ComponentesProvider provider) { + return Container( + constraints: const BoxConstraints(maxWidth: 300), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.orange.withOpacity(0.9), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.orange), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Row( + children: [ + Icon(Icons.warning, color: Colors.white, size: 16), + SizedBox(width: 8), + Text( + 'Alertas de Topología', + style: TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 8), + ...provider.problemasTopologia.take(3).map((problema) => Padding( + padding: const EdgeInsets.only(bottom: 4), + child: Text( + '• $problema', + style: const TextStyle(color: Colors.white, fontSize: 10), + ), + )), + if (provider.problemasTopologia.length > 3) + Text( + '... y ${provider.problemasTopologia.length - 3} más', + style: const TextStyle(color: Colors.white70, fontSize: 9), + ), + ], + ), + ).animate().fadeIn(delay: 1500.ms).slideY(begin: 0.3); + } + + Widget _buildRackView(bool isMediumScreen, ComponentesProvider provider) { return Container( padding: const EdgeInsets.all(24), - child: const Center( - child: Text( - 'Vista de Racks - En desarrollo', - style: TextStyle(color: Colors.white, fontSize: 18), + 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), + ), + ], ), ), ); } - Widget _buildFloorPlanView(bool isMediumScreen) { + Widget _buildFloorPlanView( + bool isMediumScreen, ComponentesProvider provider) { return Container( padding: const EdgeInsets.all(24), - child: const Center( - child: Text( - 'Plano de Planta - En desarrollo', - style: TextStyle(color: Colors.white, fontSize: 18), + 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), + ), + ], ), ), ); @@ -924,7 +1267,7 @@ class _TopologiaPageState extends State return Icons.router; case 'IDF': return Icons.hub; - case 'AccessSwitch': + case 'Switch': return Icons.network_check; case 'Server': return Icons.dns; @@ -941,7 +1284,7 @@ class _TopologiaPageState extends State return const Color(0xFF2196F3); case 'IDF': return const Color(0xFF4CAF50); - case 'AccessSwitch': + case 'Switch': return const Color(0xFF9C27B0); case 'Server': return const Color(0xFFE91E63); @@ -967,533 +1310,33 @@ class _TopologiaPageState extends State } } - Future _loadRealTopologyData() async { - setState(() { - _isLoading = true; - }); + Future _loadTopologyData() async { + final provider = Provider.of(context, listen: false); - try { - final componentesProvider = - Provider.of(context, listen: false); + if (provider.negocioSeleccionadoId != null) { + setState(() { + _isLoading = true; + }); - // Verificar que hay un negocio seleccionado en el provider - if (componentesProvider.negocioSeleccionadoId == null) { - // Si no hay negocio seleccionado, mostrar mensaje - setState(() { - _isLoading = false; - }); - _showNoBusinessSelectedDialog(); - return; - } + await provider.getTopologiaPorNegocio(provider.negocioSeleccionadoId!); - // Cargar toda la topología del negocio seleccionado usando el método optimizado - await componentesProvider.cargarTopologiaCompletaOptimizada( - componentesProvider.negocioSeleccionadoId!); - - // Mostrar estadísticas detalladas para debug - _mostrarEstadisticasComponentes(); - - // Construir la topología con datos reales del negocio seleccionado - await _buildRealNetworkTopologyOptimized(); - } catch (e) { - print('Error al cargar datos de topología: ${e.toString()}'); - _showErrorDialog('Error al cargar la topología: ${e.toString()}'); - } finally { setState(() { _isLoading = false; }); } } - Future _buildRealNetworkTopologyOptimized() async { - dashboard.removeAllElements(); + Future _refreshTopology(ComponentesProvider provider) async { + if (provider.negocioSeleccionadoId != null) { + setState(() { + _isLoading = true; + }); - final componentesProvider = - Provider.of(context, listen: false); + await provider.getTopologiaPorNegocio(provider.negocioSeleccionadoId!); - // Usar los datos optimizados - final mdfComponents = componentesProvider.getComponentesMDFOptimizados(); - final idfComponents = componentesProvider.getComponentesIDFOptimizados(); - final switchesAcceso = componentesProvider - .getComponentesPorTipoOptimizado('switch') - .where((s) => !s.esMDF && !s.esIDF) - .toList(); - final routers = - componentesProvider.getComponentesPorTipoOptimizado('router'); - final servidores = - componentesProvider.getComponentesPorTipoOptimizado('servidor'); - - print('Componentes optimizados encontrados:'); - print('- MDF: ${mdfComponents.length}'); - print('- IDF: ${idfComponents.length}'); - print('- Switches de acceso: ${switchesAcceso.length}'); - print('- Routers: ${routers.length}'); - print('- Servidores: ${servidores.length}'); - - double currentX = 100; - double currentY = 100; - const double espacioX = 200; - const double espacioY = 150; - - Map elementosMap = {}; - - // Crear elementos MDF usando datos optimizados - if (mdfComponents.isNotEmpty) { - final mdfElement = _createMDFElementOptimized( - mdfComponents, Offset(currentX + espacioX * 2, currentY)); - dashboard.addElement(mdfElement); - elementosMap[mdfComponents.first.componenteId] = mdfElement; - currentY += espacioY; + setState(() { + _isLoading = false; + }); } - - // Crear elementos IDF usando datos optimizados - double idfX = currentX; - for (var idfComp in idfComponents) { - final idfElement = _createIDFElementOptimized( - idfComp, Offset(idfX, currentY + espacioY)); - dashboard.addElement(idfElement); - elementosMap[idfComp.componenteId] = idfElement; - idfX += espacioX; - } - - // Crear switches de acceso usando datos optimizados - double switchX = currentX; - currentY += espacioY * 2; - for (var switchComp in switchesAcceso) { - final switchElement = - _createSwitchElementOptimized(switchComp, Offset(switchX, currentY)); - dashboard.addElement(switchElement); - elementosMap[switchComp.componenteId] = switchElement; - switchX += espacioX * 0.8; - } - - // Crear servidores usando datos optimizados - if (servidores.isNotEmpty) { - currentY += espacioY; - double serverX = currentX + espacioX; - for (var servidor in servidores) { - final serverElement = - _createServerElementOptimized(servidor, Offset(serverX, currentY)); - dashboard.addElement(serverElement); - elementosMap[servidor.componenteId] = serverElement; - serverX += espacioX * 0.8; - } - } - - // Crear routers/firewalls usando datos optimizados - if (routers.isNotEmpty) { - double routerX = currentX; - for (var router in routers) { - final routerElement = _createRouterElementOptimized( - router, Offset(routerX, currentY - espacioY * 3)); - dashboard.addElement(routerElement); - elementosMap[router.componenteId] = routerElement; - routerX += espacioX; - } - } - - // Crear conexiones basadas en la vista optimizada de cables - await _createOptimizedConnections(elementosMap, componentesProvider); - - // Si no hay elementos reales, mostrar mensaje - if (elementosMap.isEmpty) { - _showNoComponentsMessage(); - } - - setState(() {}); - } - - FlowElement _createMDFElementOptimized( - List mdfComponents, Offset position) { - final mainComponent = mdfComponents.first; - - return FlowElement( - position: position, - size: const Size(180, 140), - text: 'MDF\n${mainComponent.componenteNombre}', - textColor: Colors.white, - textSize: 14, - textIsBold: true, - kind: ElementKind.rectangle, - backgroundColor: const Color(0xFF2196F3), - borderColor: const Color(0xFF1976D2), - borderThickness: 3, - elevation: 8, - data: { - 'type': 'MDF', - 'componenteId': mainComponent.componenteId, - 'name': mainComponent.componenteNombre, - 'status': mainComponent.activo ? 'active' : 'disconnected', - 'description': mainComponent.descripcion ?? 'Main Distribution Frame', - 'ubicacion': mainComponent.ubicacion ?? 'Sin ubicación', - 'categoria': mainComponent.categoriaComponente, - 'componentes': mdfComponents.length, - 'distribucion': mainComponent.distribucionNombre ?? 'MDF Principal', - }, - handlers: [ - Handler.bottomCenter, - Handler.leftCenter, - Handler.rightCenter, - ], - ); - } - - FlowElement _createIDFElementOptimized( - VistaTopologiaPorNegocio idfComponent, Offset position) { - return FlowElement( - position: position, - size: const Size(160, 120), - text: 'IDF\n${idfComponent.componenteNombre}', - textColor: Colors.white, - textSize: 12, - textIsBold: true, - kind: ElementKind.rectangle, - backgroundColor: idfComponent.enUso - ? const Color(0xFF4CAF50) - : const Color(0xFFFF9800), - borderColor: idfComponent.enUso - ? const Color(0xFF388E3C) - : const Color(0xFFF57C00), - borderThickness: 2, - elevation: 6, - data: { - 'type': 'IDF', - 'componenteId': idfComponent.componenteId, - 'name': idfComponent.componenteNombre, - 'status': idfComponent.activo - ? (idfComponent.enUso ? 'active' : 'warning') - : 'disconnected', - 'description': - idfComponent.descripcion ?? 'Intermediate Distribution Frame', - 'ubicacion': idfComponent.ubicacion ?? 'Sin ubicación', - 'categoria': idfComponent.categoriaComponente, - 'enUso': idfComponent.enUso, - 'distribucion': idfComponent.distribucionNombre ?? 'IDF', - }, - handlers: [ - Handler.topCenter, - Handler.bottomCenter, - Handler.leftCenter, - Handler.rightCenter, - ], - ); - } - - FlowElement _createSwitchElementOptimized( - VistaTopologiaPorNegocio switchComponent, Offset position) { - return FlowElement( - position: position, - size: const Size(140, 100), - text: 'Switch\n${switchComponent.componenteNombre}', - textColor: Colors.white, - textSize: 10, - textIsBold: true, - kind: ElementKind.rectangle, - backgroundColor: switchComponent.activo - ? const Color(0xFF9C27B0) - : const Color(0xFF757575), - borderColor: switchComponent.activo - ? const Color(0xFF7B1FA2) - : const Color(0xFF424242), - borderThickness: 2, - elevation: switchComponent.activo ? 4 : 2, - data: { - 'type': 'AccessSwitch', - 'componenteId': switchComponent.componenteId, - 'name': switchComponent.componenteNombre, - 'status': switchComponent.activo ? 'active' : 'disconnected', - 'description': switchComponent.descripcion ?? 'Switch de Acceso', - 'ubicacion': switchComponent.ubicacion ?? 'Sin ubicación', - 'categoria': switchComponent.categoriaComponente, - 'enUso': switchComponent.enUso, - }, - handlers: [ - Handler.topCenter, - ], - ); - } - - FlowElement _createServerElementOptimized( - VistaTopologiaPorNegocio serverComponent, Offset position) { - return FlowElement( - position: position, - size: const Size(150, 100), - text: 'Servidor\n${serverComponent.componenteNombre}', - textColor: Colors.white, - textSize: 11, - textIsBold: true, - kind: ElementKind.rectangle, - backgroundColor: serverComponent.activo - ? const Color(0xFFE91E63) - : const Color(0xFF757575), - borderColor: serverComponent.activo - ? const Color(0xFFC2185B) - : const Color(0xFF424242), - borderThickness: 3, - elevation: serverComponent.activo ? 6 : 2, - data: { - 'type': 'Server', - 'componenteId': serverComponent.componenteId, - 'name': serverComponent.componenteNombre, - 'status': serverComponent.activo ? 'active' : 'disconnected', - 'description': serverComponent.descripcion ?? 'Servidor de red', - 'ubicacion': serverComponent.ubicacion ?? 'Sin ubicación', - 'categoria': serverComponent.categoriaComponente, - 'enUso': serverComponent.enUso, - }, - handlers: [ - Handler.topCenter, - Handler.leftCenter, - Handler.rightCenter, - ], - ); - } - - FlowElement _createRouterElementOptimized( - VistaTopologiaPorNegocio routerComponent, Offset position) { - return FlowElement( - position: position, - size: const Size(160, 100), - text: 'Router\n${routerComponent.componenteNombre}', - textColor: Colors.white, - textSize: 11, - textIsBold: true, - kind: ElementKind.rectangle, - backgroundColor: routerComponent.activo - ? const Color(0xFFFF5722) - : const Color(0xFF757575), - borderColor: routerComponent.activo - ? const Color(0xFFE64A19) - : const Color(0xFF424242), - borderThickness: 3, - elevation: routerComponent.activo ? 6 : 2, - data: { - 'type': 'Router', - 'componenteId': routerComponent.componenteId, - 'name': routerComponent.componenteNombre, - 'status': routerComponent.activo ? 'active' : 'disconnected', - 'description': routerComponent.descripcion ?? 'Router/Firewall', - 'ubicacion': routerComponent.ubicacion ?? 'Sin ubicación', - 'categoria': routerComponent.categoriaComponente, - 'enUso': routerComponent.enUso, - }, - handlers: [ - Handler.topCenter, - Handler.bottomCenter, - Handler.leftCenter, - Handler.rightCenter, - ], - ); - } - - Future _createOptimizedConnections( - Map elementosMap, - ComponentesProvider componentesProvider) async { - try { - print('Creando conexiones optimizadas...'); - print( - 'Conexiones con cables encontradas: ${componentesProvider.conexionesConCables.length}'); - - for (var conexionCable in componentesProvider.conexionesConCables) { - if (!conexionCable.activo) continue; - - final elementoOrigen = elementosMap[conexionCable.origenId]; - final elementoDestino = elementosMap[conexionCable.destinoId]; - - if (elementoOrigen != null && elementoDestino != null) { - // Usar la información real del cable para determinar color y grosor - final colorConexion = _getColorFromCableData(conexionCable); - final grosorConexion = _getThicknessFromCableData(conexionCable); - - print( - 'Creando conexión: ${conexionCable.componenteOrigen} -> ${conexionCable.componenteDestino}'); - if (conexionCable.tipoCable != null) { - print( - ' Cable: ${conexionCable.tipoCable} (${conexionCable.color ?? 'sin color'})'); - } - - // Crear la conexión en el FlowChart - elementoOrigen.next ??= []; - - elementoOrigen.next!.add( - ConnectionParams( - destElementId: elementoDestino.id, - arrowParams: ArrowParams( - color: colorConexion, - thickness: grosorConexion, - ), - ), - ); - } else { - print( - 'Elementos no encontrados para conexión: ${conexionCable.origenId} -> ${conexionCable.destinoId}'); - } - } - - setState(() {}); - } catch (e) { - print('Error en _createOptimizedConnections: ${e.toString()}'); - } - } - - Color _getColorFromCableData(VistaConexionesPorCables conexionCable) { - // Usar el método del modelo para obtener el color - final colorHex = conexionCable.getColorForVisualization(); - return _hexToColor(colorHex); - } - - double _getThicknessFromCableData(VistaConexionesPorCables conexionCable) { - // Usar el método del modelo para obtener el grosor - return conexionCable.getThicknessForVisualization(); - } - - Color _hexToColor(String hex) { - hex = hex.replaceAll('#', ''); - return Color(int.parse('FF$hex', radix: 16)); - } - - void _showNoBusinessSelectedDialog() { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Row( - children: [ - Icon(Icons.business, color: Colors.orange), - SizedBox(width: 8), - Text('Negocio no seleccionado'), - ], - ), - content: const Text( - 'Para visualizar la topología de red, primero debe seleccionar un negocio desde la página de empresas.', - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Entendido'), - ), - ElevatedButton( - onPressed: () { - Navigator.of(context).pop(); - // Navegar a la página de empresas - // router.pushNamed('/infrastructure/empresas'); - }, - child: const Text('Ir a Empresas'), - ), - ], - ), - ); - } - - void _showErrorDialog(String mensaje) { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Row( - children: [ - Icon(Icons.error, color: Colors.red), - SizedBox(width: 8), - Text('Error'), - ], - ), - content: Text(mensaje), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Cerrar'), - ), - ], - ), - ); - } - - void _showNoComponentsMessage() { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Row( - children: [ - Icon(Icons.info, color: Colors.blue), - SizedBox(width: 8), - Text('Sin componentes'), - ], - ), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'No se encontraron componentes de infraestructura para este negocio.', - ), - const SizedBox(height: 16), - const Text( - 'Para visualizar la topología, necesita:', - style: TextStyle(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), - const Text('• Registrar componentes (switches, routers, etc.)'), - const Text('• Configurar distribuciones (MDF/IDF)'), - const Text('• Crear conexiones entre componentes'), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Entendido'), - ), - ElevatedButton( - onPressed: () { - Navigator.of(context).pop(); - // Navegar a la página de componentes - // router.pushNamed('/infrastructure/componentes'); - }, - child: const Text('Gestionar Componentes'), - ), - ], - ), - ); - } - - // Método para mostrar estadísticas detalladas de componentes - void _mostrarEstadisticasComponentes() { - final provider = Provider.of(context, listen: false); - print('\n=== ESTADÍSTICAS DETALLADAS DE COMPONENTES ==='); - print('Total de componentes cargados: ${provider.topologiaOptimizada.length}'); - - if (provider.topologiaOptimizada.isEmpty) { - print('❌ No hay componentes cargados'); - return; - } - - // Agrupar por categoría - final componentesPorCategoria = >{}; - for (final componente in provider.topologiaOptimizada) { - final categoria = componente.categoriaComponente; - componentesPorCategoria.putIfAbsent(categoria, () => []).add(componente); - } - - print('\n📊 Componentes por categoría:'); - componentesPorCategoria.forEach((categoria, lista) { - print(' - $categoria: ${lista.length} componentes'); - for (final comp in lista) { - print(' • ${comp.componenteNombre} (${comp.tipoComponentePrincipal}) - Activo: ${comp.activo}'); - } - }); - - // Clasificación por tipo principal - final mdfComponents = provider.getComponentesMDFOptimizados(); - final idfComponents = provider.getComponentesIDFOptimizados(); - final switchComponents = provider.getComponentesPorTipoOptimizado('switch'); - final routerComponents = provider.getComponentesPorTipoOptimizado('router'); - final servidorComponents = provider.getComponentesPorTipoOptimizado('servidor'); - - print('\n🔧 Clasificación por tipo:'); - print(' - MDF: ${mdfComponents.length}'); - print(' - IDF: ${idfComponents.length}'); - print(' - Switches: ${switchComponents.length}'); - print(' - Routers: ${routerComponents.length}'); - print(' - Servidores: ${servidorComponents.length}'); - - print('\n🔗 Conexiones disponibles: ${provider.conexionesConCables.length}'); - print('===============================================\n'); } } diff --git a/lib/providers/nethive/componentes_provider.dart b/lib/providers/nethive/componentes_provider.dart index 08edaef..09d5867 100644 --- a/lib/providers/nethive/componentes_provider.dart +++ b/lib/providers/nethive/componentes_provider.dart @@ -10,6 +10,10 @@ import 'package:nethive_neo/models/nethive/categoria_componente_model.dart'; import 'package:nethive_neo/models/nethive/componente_model.dart'; import 'package:nethive_neo/models/nethive/distribucion_model.dart'; import 'package:nethive_neo/models/nethive/conexion_componente_model.dart'; +import 'package:nethive_neo/models/nethive/conexion_alimentacion_model.dart'; +import 'package:nethive_neo/models/nethive/topologia_completa_model.dart'; +import 'package:nethive_neo/models/nethive/rol_logico_componente_model.dart'; +import 'package:nethive_neo/models/nethive/tipo_distribucion_model.dart'; import 'package:nethive_neo/models/nethive/detalle_cable_model.dart'; import 'package:nethive_neo/models/nethive/detalle_switch_model.dart'; import 'package:nethive_neo/models/nethive/detalle_patch_panel_model.dart'; @@ -32,11 +36,19 @@ class ComponentesProvider extends ChangeNotifier { // Listas principales List categorias = []; + List rolesLogicos = []; + List tiposDistribucion = []; List componentes = []; List componentesRows = []; List categoriasRows = []; - // Listas para topología optimizada + // Nueva estructura de topología optimizada + TopologiaCompleta? topologiaCompleta; + List componentesTopologia = []; + List conexionesDatos = []; + List conexionesEnergia = []; + + // Listas para retrocompatibilidad List distribuciones = []; List conexiones = []; List conexionesConCables = []; @@ -69,7 +81,7 @@ class ComponentesProvider extends ChangeNotifier { bool _isDisposed = false; ComponentesProvider() { - getCategorias(); + _inicializarDatos(); } @override @@ -87,7 +99,194 @@ class ComponentesProvider extends ChangeNotifier { } } - // MÉTODOS PARA CATEGORÍAS + // INICIALIZACIÓN + Future _inicializarDatos() async { + try { + await Future.wait([ + getCategorias(), + getRolesLogicos(), + getTiposDistribucion(), + ]); + } catch (e) { + print('Error en inicialización: ${e.toString()}'); + } + } + + // MÉTODOS PARA ROLES LÓGICOS + Future getRolesLogicos() async { + try { + final res = await supabaseLU + .from('rol_logico_componente') + .select() + .order('nombre', ascending: true); + + rolesLogicos = (res as List) + .map((rol) => RolLogicoComponente.fromMap(rol)) + .toList(); + + _safeNotifyListeners(); + } catch (e) { + print('Error en getRolesLogicos: ${e.toString()}'); + } + } + + // MÉTODOS PARA TIPOS DE DISTRIBUCIÓN + Future getTiposDistribucion() async { + try { + final res = await supabaseLU + .from('tipo_distribucion') + .select() + .order('nombre', ascending: true); + + tiposDistribucion = (res as List) + .map((tipo) => TipoDistribucion.fromMap(tipo)) + .toList(); + + _safeNotifyListeners(); + } catch (e) { + print('Error en getTiposDistribucion: ${e.toString()}'); + } + } + + // MÉTODO PRINCIPAL PARA OBTENER TOPOLOGÍA COMPLETA + Future getTopologiaPorNegocio(String negocioId) async { + try { + isLoadingTopologia = true; + _safeNotifyListeners(); + + print( + 'Llamando a función RPC fn_topologia_por_negocio con negocio_id: $negocioId'); + + final response = + await supabaseLU.rpc('fn_topologia_por_negocio', params: { + 'p_negocio_id': negocioId, + }).select(); + + print('Respuesta RPC recibida: $response'); + + if (response != null) { + topologiaCompleta = TopologiaCompleta.fromJson(response); + + // Actualizar listas individuales + componentesTopologia = topologiaCompleta!.componentes; + conexionesDatos = topologiaCompleta!.conexionesDatos; + conexionesEnergia = topologiaCompleta!.conexionesEnergia; + + // Sincronizar con estructuras anteriores para retrocompatibilidad + _sincronizarEstructurasAnteriores(); + + print('Topología cargada exitosamente:'); + print('- Componentes: ${componentesTopologia.length}'); + print('- Conexiones de datos: ${conexionesDatos.length}'); + print('- Conexiones de energía: ${conexionesEnergia.length}'); + + problemasTopologia = _validarTopologiaCompleta(); + } else { + print('Respuesta RPC nula, cargando datos con métodos alternativos'); + await _cargarTopologiaAlternativa(negocioId); + } + } catch (e) { + print('Error en getTopologiaPorNegocio: ${e.toString()}'); + await _cargarTopologiaAlternativa(negocioId); + } finally { + isLoadingTopologia = false; + _safeNotifyListeners(); + } + } + + void _sincronizarEstructurasAnteriores() { + // Convertir ComponenteTopologia a Componente para retrocompatibilidad + componentes = componentesTopologia.map((ct) { + return Componente( + id: ct.id, + negocioId: negocioSeleccionadoId ?? '', + categoriaId: ct.categoriaId, + nombre: ct.nombre, + descripcion: ct.descripcion, + ubicacion: ct.ubicacion, + imagenUrl: ct.imagenUrl, + enUso: ct.enUso, + activo: ct.activo, + fechaRegistro: ct.fechaRegistro, + distribucionId: ct.distribucionId, + ); + }).toList(); + + // Convertir ConexionDatos a ConexionComponente para retrocompatibilidad + conexiones = conexionesDatos.map((cd) { + return ConexionComponente( + id: cd.id, + componenteOrigenId: cd.componenteOrigenId, + componenteDestinoId: cd.componenteDestinoId, + descripcion: cd.descripcion, + activo: cd.activo, + ); + }).toList(); + + // Construir filas para la tabla + _buildComponentesRows(); + } + + Future _cargarTopologiaAlternativa(String negocioId) async { + try { + problemasTopologia = ['Usando método alternativo de carga de datos']; + + await Future.wait([ + getComponentesPorNegocio(negocioId), + getDistribucionesPorNegocio(negocioId), + getConexionesPorNegocio(negocioId), + ]); + + problemasTopologia.addAll(validarTopologia()); + } catch (e) { + problemasTopologia = [ + 'Error al cargar datos de topología: ${e.toString()}' + ]; + } + } + + List _validarTopologiaCompleta() { + List problemas = []; + + if (componentesTopologia.isEmpty) { + problemas.add('No se encontraron componentes para este negocio'); + return problemas; + } + + final mdfComponents = componentesTopologia.where((c) => c.esMDF).toList(); + final idfComponents = componentesTopologia.where((c) => c.esIDF).toList(); + + if (mdfComponents.isEmpty) { + problemas + .add('No se encontraron componentes MDF (distribución principal)'); + } + + if (idfComponents.isEmpty) { + problemas.add( + 'No se encontraron componentes IDF (distribuciones intermedias)'); + } + + final sinUbicacion = componentesTopologia + .where((c) => + c.activo && (c.ubicacion == null || c.ubicacion!.trim().isEmpty)) + .length; + if (sinUbicacion > 0) { + problemas.add('$sinUbicacion componentes activos sin ubicación definida'); + } + + final componentesActivos = + componentesTopologia.where((c) => c.activo).length; + final conexionesDatosActivas = + conexionesDatos.where((c) => c.activo).length; + + if (componentesActivos > 1 && conexionesDatosActivas == 0) { + problemas.add('No se encontraron conexiones de datos entre componentes'); + } + + return problemas; + } + + // MÉTODOS PARA CATEGORÍAS (mantenidos para retrocompatibilidad) Future getCategorias([String? busqueda]) async { try { var query = supabaseLU.from('categoria_componente').select(); @@ -122,54 +321,7 @@ class ComponentesProvider extends ChangeNotifier { } } - Future crearCategoria(String nombre) async { - try { - final res = await supabaseLU.from('categoria_componente').insert({ - 'nombre': nombre, - }).select(); - - if (res.isNotEmpty) { - await getCategorias(); - return true; - } - return false; - } catch (e) { - print('Error en crearCategoria: ${e.toString()}'); - return false; - } - } - - Future actualizarCategoria(int id, String nombre) async { - try { - final res = await supabaseLU - .from('categoria_componente') - .update({'nombre': nombre}) - .eq('id', id) - .select(); - - if (res.isNotEmpty) { - await getCategorias(); - return true; - } - return false; - } catch (e) { - print('Error en actualizarCategoria: ${e.toString()}'); - return false; - } - } - - Future eliminarCategoria(int id) async { - try { - await supabaseLU.from('categoria_componente').delete().eq('id', id); - await getCategorias(); - return true; - } catch (e) { - print('Error en eliminarCategoria: ${e.toString()}'); - return false; - } - } - - // MÉTODOS PARA COMPONENTES + // MÉTODOS PARA COMPONENTES (mantenidos para retrocompatibilidad) Future getComponentesPorNegocio(String negocioId, [String? busqueda]) async { try { @@ -226,206 +378,7 @@ class ComponentesProvider extends ChangeNotifier { } } - Future crearComponente({ - required String negocioId, - required int categoriaId, - required String nombre, - String? descripcion, - required bool enUso, - required bool activo, - String? ubicacion, - }) async { - try { - final imagenUrl = await uploadImagen(); - - final res = await supabaseLU.from('componente').insert({ - 'negocio_id': negocioId, - 'categoria_id': categoriaId, - 'nombre': nombre, - 'descripcion': descripcion, - 'en_uso': enUso, - 'activo': activo, - 'ubicacion': ubicacion, - 'imagen_url': imagenUrl, - }).select(); - - if (res.isNotEmpty) { - await getComponentesPorNegocio(negocioId); - resetFormData(); - return true; - } - return false; - } catch (e) { - print('Error en crearComponente: ${e.toString()}'); - return false; - } - } - - Future actualizarComponente({ - required String componenteId, - required String negocioId, - required int categoriaId, - required String nombre, - String? descripcion, - required bool enUso, - required bool activo, - String? ubicacion, - bool actualizarImagen = false, - }) async { - try { - Map updateData = { - 'categoria_id': categoriaId, - 'nombre': nombre, - 'descripcion': descripcion, - 'en_uso': enUso, - 'activo': activo, - 'ubicacion': ubicacion, - }; - - if (actualizarImagen) { - final imagenUrl = await uploadImagen(); - if (imagenUrl != null) { - updateData['imagen_url'] = imagenUrl; - } - } - - final res = await supabaseLU - .from('componente') - .update(updateData) - .eq('id', componenteId) - .select(); - - if (res.isNotEmpty) { - await getComponentesPorNegocio(negocioId); - resetFormData(); - return true; - } - return false; - } catch (e) { - print('Error en actualizarComponente: ${e.toString()}'); - return false; - } - } - - Future eliminarComponente(String componenteId) async { - try { - final componenteData = await supabaseLU - .from('componente') - .select('imagen_url') - .eq('id', componenteId) - .maybeSingle(); - - String? imagenUrl; - if (componenteData != null && componenteData['imagen_url'] != null) { - imagenUrl = componenteData['imagen_url'] as String; - } - - await _eliminarDetallesComponente(componenteId); - await supabaseLU.from('componente').delete().eq('id', componenteId); - - if (!_isDisposed && negocioSeleccionadoId != null) { - await getComponentesPorNegocio(negocioSeleccionadoId!); - } - - if (imagenUrl != null) { - try { - await supabaseLU.storage - .from('nethive') - .remove(["componentes/$imagenUrl"]); - } catch (storageError) { - print( - 'Error al eliminar imagen del storage: ${storageError.toString()}'); - } - } - - return true; - } catch (e) { - print('Error en eliminarComponente: ${e.toString()}'); - return false; - } - } - - Future _eliminarDetallesComponente(String componenteId) async { - try { - await Future.wait([ - supabaseLU - .from('detalle_cable') - .delete() - .eq('componente_id', componenteId), - supabaseLU - .from('detalle_switch') - .delete() - .eq('componente_id', componenteId), - supabaseLU - .from('detalle_patch_panel') - .delete() - .eq('componente_id', componenteId), - supabaseLU - .from('detalle_rack') - .delete() - .eq('componente_id', componenteId), - supabaseLU - .from('detalle_organizador') - .delete() - .eq('componente_id', componenteId), - supabaseLU - .from('detalle_ups') - .delete() - .eq('componente_id', componenteId), - supabaseLU - .from('detalle_router_firewall') - .delete() - .eq('componente_id', componenteId), - supabaseLU - .from('detalle_equipo_activo') - .delete() - .eq('componente_id', componenteId), - supabaseLU.from('conexion_componente').delete().or( - 'componente_origen_id.eq.$componenteId,componente_destino_id.eq.$componenteId'), - ]); - } catch (e) { - print('Error al eliminar detalles del componente: ${e.toString()}'); - } - } - - // MÉTODOS PARA IMÁGENES - Future selectImagen() async { - imagenFileName = null; - imagenToUpload = null; - - FilePickerResult? picker = await FilePicker.platform.pickFiles( - type: FileType.custom, - allowedExtensions: ['jpg', 'png', 'jpeg'], - ); - - if (picker != null) { - var now = DateTime.now(); - var formatter = DateFormat('yyyyMMddHHmmss'); - var timestamp = formatter.format(now); - - imagenFileName = 'componente-$timestamp-${picker.files.single.name}'; - imagenToUpload = picker.files.single.bytes; - } - - _safeNotifyListeners(); - } - - Future uploadImagen() async { - if (imagenToUpload != null && imagenFileName != null) { - await supabaseLU.storage.from('nethive/componentes').uploadBinary( - imagenFileName!, - imagenToUpload!, - fileOptions: const FileOptions( - cacheControl: '3600', - upsert: false, - ), - ); - return imagenFileName; - } - return null; - } - - // MÉTODOS PARA DISTRIBUCIONES + // MÉTODOS PARA DISTRIBUCIONES (mantenidos para retrocompatibilidad) Future getDistribucionesPorNegocio(String negocioId) async { try { final res = await supabaseLU @@ -445,7 +398,7 @@ class ComponentesProvider extends ChangeNotifier { } } - // MÉTODOS PARA CONEXIONES + // MÉTODOS PARA CONEXIONES (mantenidos para retrocompatibilidad) Future getConexionesPorNegocio(String negocioId) async { try { List res; @@ -487,351 +440,7 @@ class ComponentesProvider extends ChangeNotifier { } } - // MÉTODOS PARA TOPOLOGÍA OPTIMIZADA - Future getConexionesConCablesPorNegocio(String negocioId) async { - try { - final res = await supabaseLU - .from('vista_conexiones_con_cables') - .select() - .eq('activo', true); - - conexionesConCables = (res as List) - .map((conexion) => VistaConexionesPorCables.fromMap(conexion)) - .toList(); - - print('Conexiones con cables cargadas: ${conexionesConCables.length}'); - } catch (e) { - print('Error en getConexionesConCablesPorNegocio: ${e.toString()}'); - await _getConexionesConCablesManual(negocioId); - } - } - - Future _getConexionesConCablesManual(String negocioId) async { - try { - final componentesDelNegocio = await supabaseLU - .from('componente') - .select('id') - .eq('negocio_id', negocioId); - - if (componentesDelNegocio.isEmpty) { - conexionesConCables = []; - return; - } - - final componenteIds = - componentesDelNegocio.map((comp) => comp['id'] as String).toList(); - - final conexionesRes = await supabaseLU - .from('conexion_componente') - .select('''' - *, - componente_origen:componente!componente_origen_id(id, nombre), - componente_destino:componente!componente_destino_id(id, nombre), - cable:componente!cable_id(id, nombre), - detalle_cable!cable_id(*) - ''') - .or('componente_origen_id.in.(${componenteIds.join(',')}),componente_destino_id.in.(${componenteIds.join(',')})') - .eq('activo', true); - - conexionesConCables = (conexionesRes as List).map((conexion) { - final origenData = - conexion['componente_origen'] as Map?; - final destinoData = - conexion['componente_destino'] as Map?; - final cableData = conexion['cable'] as Map?; - final detalleCableData = - conexion['detalle_cable'] as Map?; - - return VistaConexionesPorCables( - conexionId: conexion['id'], - descripcion: conexion['descripcion'], - activo: conexion['activo'] ?? false, - origenId: origenData?['id'] ?? '', - componenteOrigen: origenData?['nombre'] ?? '', - destinoId: destinoData?['id'] ?? '', - componenteDestino: destinoData?['nombre'] ?? '', - cableId: cableData?['id'], - cableUsado: cableData?['nombre'], - tipoCable: detalleCableData?['tipo_cable'], - color: detalleCableData?['color'], - tamano: detalleCableData?['tamaño']?.toDouble(), - tipoConector: detalleCableData?['tipo_conector'], - ); - }).toList(); - - print('Conexiones con cables (manual): ${conexionesConCables.length}'); - } catch (e) { - print('Error en _getConexionesConCablesManual: ${e.toString()}'); - conexionesConCables = []; - } - } - - Future getTopologiaOptimizadaPorNegocio(String negocioId) async { - try { - final res = await supabaseLU - .from('vista_topologia_por_negocio') - .select() - .eq('negocio_id', negocioId) - .eq('activo', true) - .order('fecha_registro', ascending: false); - - topologiaOptimizada = (res as List) - .map((item) => VistaTopologiaPorNegocio.fromMap(item)) - .toList(); - - print( - 'Topología optimizada cargada: ${topologiaOptimizada.length} componentes'); - } catch (e) { - print('Error en getTopologiaOptimizadaPorNegocio: ${e.toString()}'); - await _getTopologiaOptimizadaManual(negocioId); - } - } - - Future _getTopologiaOptimizadaManual(String negocioId) async { - try { - final res = await supabaseLU - .from('componente') - .select(''' - *, - categoria_componente!inner(id, nombre), - distribucion(id, tipo, nombre), - negocio!inner(id, nombre) - ''') - .eq('negocio_id', negocioId) - .eq('activo', true) - .order('fecha_registro', ascending: false); - - topologiaOptimizada = (res as List).map((item) { - // Manejar datos de negocio - final negocioData = item['negocio']; - final String negocioIdRes = negocioData is Map - ? negocioData['id']?.toString() ?? negocioId - : negocioId; - final String nombreNegocio = negocioData is Map - ? negocioData['nombre']?.toString() ?? 'Sin nombre' - : 'Sin nombre'; - - // Manejar datos de categoría - final categoriaData = item['categoria_componente']; - final String nombreCategoria = categoriaData is Map - ? categoriaData['nombre']?.toString() ?? 'Sin categoría' - : 'Sin categoría'; - - // Manejar datos de distribución (puede ser null) - final distribucionData = item['distribucion']; - String? distribucionId; - String? tipoDistribucion; - String? distribucionNombre; - - if (distribucionData is Map) { - distribucionId = distribucionData['id']?.toString(); - tipoDistribucion = distribucionData['tipo']?.toString(); - distribucionNombre = distribucionData['nombre']?.toString(); - } - - return VistaTopologiaPorNegocio( - negocioId: negocioIdRes, - nombreNegocio: nombreNegocio, - distribucionId: distribucionId, - tipoDistribucion: tipoDistribucion, - distribucionNombre: distribucionNombre, - componenteId: item['id']?.toString() ?? '', - componenteNombre: item['nombre']?.toString() ?? '', - descripcion: item['descripcion']?.toString(), - categoriaComponente: nombreCategoria, - enUso: item['en_uso'] == true, - activo: item['activo'] == true, - ubicacion: item['ubicacion']?.toString(), - imagenUrl: item['imagen_url']?.toString(), - fechaRegistro: - DateTime.tryParse(item['fecha_registro']?.toString() ?? '') ?? - DateTime.now(), - ); - }).toList(); - - print( - 'Topología optimizada (manual): ${topologiaOptimizada.length} componentes'); - - // Debug: Mostrar las categorías encontradas - final categoriasEncontradas = topologiaOptimizada - .map((c) => c.categoriaComponente) - .toSet() - .toList(); - print('Categorías encontradas: $categoriasEncontradas'); - } catch (e) { - print('Error en _getTopologiaOptimizadaManual: ${e.toString()}'); - topologiaOptimizada = []; - } - } - - Future cargarTopologiaCompletaOptimizada(String negocioId) async { - isLoadingTopologia = true; - _safeNotifyListeners(); - - try { - await Future.wait([ - getTopologiaOptimizadaPorNegocio(negocioId), - getConexionesConCablesPorNegocio(negocioId), - getDistribucionesPorNegocio(negocioId), - ]); - - _sincronizarConListasPrincipales(); - problemasTopologia = validarTopologiaOptimizada(); - print('Topología completa cargada exitosamente'); - } catch (e) { - print('Error en cargarTopologiaCompletaOptimizada: ${e.toString()}'); - problemasTopologia = [ - 'Error al cargar datos de topología optimizada: ${e.toString()}' - ]; - await cargarTopologiaCompleta(negocioId); - } finally { - isLoadingTopologia = false; - _safeNotifyListeners(); - } - } - - void _sincronizarConListasPrincipales() { - if (topologiaOptimizada.isNotEmpty) { - componentes = topologiaOptimizada.map((topo) { - return Componente( - id: topo.componenteId, - negocioId: topo.negocioId, - categoriaId: _getCategoriaIdPorNombre(topo.categoriaComponente), - nombre: topo.componenteNombre, - descripcion: topo.descripcion, - ubicacion: topo.ubicacion, - imagenUrl: topo.imagenUrl, - enUso: topo.enUso, - activo: topo.activo, - fechaRegistro: topo.fechaRegistro, - distribucionId: topo.distribucionId, - ); - }).toList(); - - _buildComponentesRows(); - } - - if (conexionesConCables.isNotEmpty) { - conexiones = conexionesConCables.map((conexionCable) { - return ConexionComponente( - id: conexionCable.conexionId, - componenteOrigenId: conexionCable.origenId, - componenteDestinoId: conexionCable.destinoId, - descripcion: conexionCable.descripcion, - activo: conexionCable.activo, - ); - }).toList(); - } - } - - // MÉTODOS DE GESTIÓN DE TOPOLOGÍA - Future setNegocioSeleccionado( - String negocioId, String negocioNombre, String empresaId) async { - try { - negocioSeleccionadoId = negocioId; - negocioSeleccionadoNombre = negocioNombre; - empresaSeleccionadaId = empresaId; - - _limpiarDatosAnteriores(); - await cargarTopologiaCompleta(negocioId); - _safeNotifyListeners(); - } catch (e) { - print('Error en setNegocioSeleccionado: ${e.toString()}'); - } - } - - void _limpiarDatosAnteriores() { - componentes.clear(); - distribuciones.clear(); - conexiones.clear(); - conexionesConCables.clear(); - topologiaOptimizada.clear(); - componentesRows.clear(); - showDetallesEspecificos = false; - - detalleCable = null; - detalleSwitch = null; - detallePatchPanel = null; - detalleRack = null; - detalleOrganizador = null; - detalleUps = null; - detalleRouterFirewall = null; - detalleEquipoActivo = null; - } - - Future cargarTopologiaCompleta(String negocioId) async { - isLoadingTopologia = true; - _safeNotifyListeners(); - - try { - await Future.wait([ - getComponentesPorNegocio(negocioId), - getDistribucionesPorNegocio(negocioId), - getConexionesPorNegocio(negocioId), - ]); - - problemasTopologia = validarTopologia(); - print( - 'Topología cargada - Componentes: ${componentes.length}, Distribuciones: ${distribuciones.length}, Conexiones: ${conexiones.length}'); - } catch (e) { - print('Error en cargarTopologiaCompleta: ${e.toString()}'); - problemasTopologia = [ - 'Error al cargar datos de topología: ${e.toString()}' - ]; - } finally { - isLoadingTopologia = false; - _safeNotifyListeners(); - } - } - - // MÉTODOS DE UTILIDAD - CategoriaComponente? getCategoriaById(int categoriaId) { - try { - return categorias.firstWhere((c) => c.id == categoriaId); - } catch (e) { - return null; - } - } - - Componente? getComponenteById(String componenteId) { - try { - return componentes.firstWhere((c) => c.id == componenteId); - } catch (e) { - return null; - } - } - - List getComponentesPorTipo(String tipo) { - return componentes.where((c) { - if (!c.activo) return false; - - final categoria = getCategoriaById(c.categoriaId); - final nombreCategoria = categoria?.nombre.toLowerCase() ?? ''; - - switch (tipo.toLowerCase()) { - case 'mdf': - return c.ubicacion?.toLowerCase().contains('mdf') == true || - nombreCategoria.contains('mdf') || - c.descripcion?.toLowerCase().contains('mdf') == true; - case 'idf': - return c.ubicacion?.toLowerCase().contains('idf') == true || - nombreCategoria.contains('idf') || - c.descripcion?.toLowerCase().contains('idf') == true; - case 'switch': - return nombreCategoria.contains('switch'); - case 'router': - return nombreCategoria.contains('router') || - nombreCategoria.contains('firewall'); - case 'servidor': - case 'server': - return nombreCategoria.contains('servidor') || - nombreCategoria.contains('server'); - default: - return false; - } - }).toList(); - } - + // MÉTODOS DE VALIDACIÓN (mantenidos para retrocompatibilidad) List validarTopologia() { List problemas = []; @@ -871,70 +480,100 @@ class ComponentesProvider extends ChangeNotifier { return problemas; } - List validarTopologiaOptimizada() { - List problemas = []; - - if (topologiaOptimizada.isEmpty) { - problemas.add('No se encontraron componentes para este negocio'); - return problemas; - } - - final mdfComponents = topologiaOptimizada.where((c) => c.esMDF).toList(); - final idfComponents = topologiaOptimizada.where((c) => c.esIDF).toList(); - - if (mdfComponents.isEmpty) { - problemas.add('No se encontraron componentes configurados como MDF'); - } - - if (idfComponents.isEmpty) { - problemas.add('No se encontraron componentes configurados como IDF'); - } - - final sinUbicacion = topologiaOptimizada - .where((c) => c.ubicacion == null || c.ubicacion!.trim().isEmpty) - .length; - - if (sinUbicacion > 0) { - problemas.add('$sinUbicacion componentes sin ubicación definida'); - } - - if (conexionesConCables.isEmpty && topologiaOptimizada.length > 1) { - problemas.add('No se encontraron conexiones entre componentes'); - } - - return problemas; - } - - // MÉTODOS PARA VISTAS OPTIMIZADAS - List getComponentesMDFOptimizados() { - return topologiaOptimizada.where((c) => c.esMDF).toList() - ..sort((a, b) => a.prioridadTopologia.compareTo(b.prioridadTopologia)); - } - - List getComponentesIDFOptimizados() { - return topologiaOptimizada.where((c) => c.esIDF).toList() - ..sort((a, b) => a.prioridadTopologia.compareTo(b.prioridadTopologia)); - } - - List getComponentesPorTipoOptimizado(String tipo) { - return topologiaOptimizada - .where((c) => c.tipoComponentePrincipal == tipo) - .toList() - ..sort((a, b) => a.prioridadTopologia.compareTo(b.prioridadTopologia)); - } - - int _getCategoriaIdPorNombre(String nombreCategoria) { + // MÉTODOS DE UTILIDAD (mantenidos) + CategoriaComponente? getCategoriaById(int categoriaId) { try { - final categoria = categorias.firstWhere( - (c) => c.nombre.toLowerCase() == nombreCategoria.toLowerCase(), - ); - return categoria.id; + return categorias.firstWhere((c) => c.id == categoriaId); } catch (e) { - return 1; + return null; } } - // MÉTODOS DE UTILIDAD PARA FORMULARIOS + Componente? getComponenteById(String componenteId) { + try { + return componentes.firstWhere((c) => c.id == componenteId); + } catch (e) { + return null; + } + } + + ComponenteTopologia? getComponenteTopologiaById(String componenteId) { + try { + return componentesTopologia.firstWhere((c) => c.id == componenteId); + } catch (e) { + return null; + } + } + + List getComponentesPorTipo(String tipo) { + return componentes.where((c) { + if (!c.activo) return false; + + final categoria = getCategoriaById(c.categoriaId); + final nombreCategoria = categoria?.nombre.toLowerCase() ?? ''; + + switch (tipo.toLowerCase()) { + case 'mdf': + return c.ubicacion?.toLowerCase().contains('mdf') == true || + nombreCategoria.contains('mdf') || + c.descripcion?.toLowerCase().contains('mdf') == true; + case 'idf': + return c.ubicacion?.toLowerCase().contains('idf') == true || + nombreCategoria.contains('idf') || + c.descripcion?.toLowerCase().contains('idf') == true; + case 'switch': + return nombreCategoria.contains('switch'); + case 'router': + return nombreCategoria.contains('router') || + nombreCategoria.contains('firewall'); + case 'servidor': + case 'server': + return nombreCategoria.contains('servidor') || + nombreCategoria.contains('server'); + default: + return false; + } + }).toList(); + } + + // MÉTODOS PARA IMÁGENES (mantenidos) + Future selectImagen() async { + imagenFileName = null; + imagenToUpload = null; + + FilePickerResult? picker = await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: ['jpg', 'png', 'jpeg'], + ); + + if (picker != null) { + var now = DateTime.now(); + var formatter = DateFormat('yyyyMMddHHmmss'); + var timestamp = formatter.format(now); + + imagenFileName = 'componente-$timestamp-${picker.files.single.name}'; + imagenToUpload = picker.files.single.bytes; + } + + _safeNotifyListeners(); + } + + Future uploadImagen() async { + if (imagenToUpload != null && imagenFileName != null) { + await supabaseLU.storage.from('nethive/componentes').uploadBinary( + imagenFileName!, + imagenToUpload!, + fileOptions: const FileOptions( + cacheControl: '3600', + upsert: false, + ), + ); + return imagenFileName; + } + return null; + } + + // MÉTODOS DE UTILIDAD PARA FORMULARIOS (mantenidos) void resetFormData() { imagenFileName = null; imagenToUpload = null; @@ -1007,4 +646,319 @@ class ComponentesProvider extends ChangeNotifier { fit: BoxFit.cover, ); } + + // MÉTODOS CRUD MANTENIDOS PARA COMPATIBILIDAD + Future crearComponente({ + required String negocioId, + required int categoriaId, + required String nombre, + String? descripcion, + required bool enUso, + required bool activo, + String? ubicacion, + }) async { + try { + final imagenUrl = await uploadImagen(); + + final res = await supabaseLU.from('componente').insert({ + 'negocio_id': negocioId, + 'categoria_id': categoriaId, + 'nombre': nombre, + 'descripcion': descripcion, + 'en_uso': enUso, + 'activo': activo, + 'ubicacion': ubicacion, + 'imagen_url': imagenUrl, + }).select(); + + if (res.isNotEmpty) { + await getTopologiaPorNegocio(negocioId); + resetFormData(); + return true; + } + return false; + } catch (e) { + print('Error en crearComponente: ${e.toString()}'); + return false; + } + } + + Future actualizarComponente({ + required String componenteId, + required String negocioId, + required int categoriaId, + required String nombre, + String? descripcion, + required bool enUso, + required bool activo, + String? ubicacion, + bool actualizarImagen = false, + }) async { + try { + Map updateData = { + 'categoria_id': categoriaId, + 'nombre': nombre, + 'descripcion': descripcion, + 'en_uso': enUso, + 'activo': activo, + 'ubicacion': ubicacion, + }; + + if (actualizarImagen) { + final imagenUrl = await uploadImagen(); + if (imagenUrl != null) { + updateData['imagen_url'] = imagenUrl; + } + } + + final res = await supabaseLU + .from('componente') + .update(updateData) + .eq('id', componenteId) + .select(); + + if (res.isNotEmpty) { + await getTopologiaPorNegocio(negocioId); + resetFormData(); + return true; + } + return false; + } catch (e) { + print('Error en actualizarComponente: ${e.toString()}'); + return false; + } + } + + Future eliminarComponente(String componenteId) async { + try { + final componenteData = await supabaseLU + .from('componente') + .select('imagen_url') + .eq('id', componenteId) + .maybeSingle(); + + String? imagenUrl; + if (componenteData != null && componenteData['imagen_url'] != null) { + imagenUrl = componenteData['imagen_url'] as String; + } + + await _eliminarDetallesComponente(componenteId); + await supabaseLU.from('componente').delete().eq('id', componenteId); + + if (!_isDisposed && negocioSeleccionadoId != null) { + await getTopologiaPorNegocio(negocioSeleccionadoId!); + } + + if (imagenUrl != null) { + try { + await supabaseLU.storage + .from('nethive') + .remove(["componentes/$imagenUrl"]); + } catch (storageError) { + print( + 'Error al eliminar imagen del storage: ${storageError.toString()}'); + } + } + + return true; + } catch (e) { + print('Error en eliminarComponente: ${e.toString()}'); + return false; + } + } + + Future _eliminarDetallesComponente(String componenteId) async { + try { + await Future.wait([ + supabaseLU + .from('detalle_cable') + .delete() + .eq('componente_id', componenteId), + supabaseLU + .from('detalle_switch') + .delete() + .eq('componente_id', componenteId), + supabaseLU + .from('detalle_patch_panel') + .delete() + .eq('componente_id', componenteId), + supabaseLU + .from('detalle_rack') + .delete() + .eq('componente_id', componenteId), + supabaseLU + .from('detalle_organizador') + .delete() + .eq('componente_id', componenteId), + supabaseLU + .from('detalle_ups') + .delete() + .eq('componente_id', componenteId), + supabaseLU + .from('detalle_router_firewall') + .delete() + .eq('componente_id', componenteId), + supabaseLU + .from('detalle_equipo_activo') + .delete() + .eq('componente_id', componenteId), + supabaseLU.from('conexion_componente').delete().or( + 'componente_origen_id.eq.$componenteId,componente_destino_id.eq.$componenteId'), + ]); + } catch (e) { + print('Error al eliminar detalles del componente: ${e.toString()}'); + } + } + + Future crearCategoria(String nombre) async { + try { + final res = await supabaseLU.from('categoria_componente').insert({ + 'nombre': nombre, + }).select(); + + if (res.isNotEmpty) { + await getCategorias(); + return true; + } + return false; + } catch (e) { + print('Error en crearCategoria: ${e.toString()}'); + return false; + } + } + + Future actualizarCategoria(int id, String nombre) async { + try { + final res = await supabaseLU + .from('categoria_componente') + .update({'nombre': nombre}) + .eq('id', id) + .select(); + + if (res.isNotEmpty) { + await getCategorias(); + return true; + } + return false; + } catch (e) { + print('Error en actualizarCategoria: ${e.toString()}'); + return false; + } + } + + Future eliminarCategoria(int id) async { + try { + await supabaseLU.from('categoria_componente').delete().eq('id', id); + await getCategorias(); + return true; + } catch (e) { + print('Error en eliminarCategoria: ${e.toString()}'); + return false; + } + } + + // MÉTODOS DE GESTIÓN DE TOPOLOGÍA OPTIMIZADA + Future setNegocioSeleccionado( + String negocioId, String negocioNombre, String empresaId) async { + try { + negocioSeleccionadoId = negocioId; + negocioSeleccionadoNombre = negocioNombre; + empresaSeleccionadaId = empresaId; + + _limpiarDatosAnteriores(); + await getTopologiaPorNegocio(negocioId); + _safeNotifyListeners(); + } catch (e) { + print('Error en setNegocioSeleccionado: ${e.toString()}'); + } + } + + void _limpiarDatosAnteriores() { + topologiaCompleta = null; + componentesTopologia.clear(); + conexionesDatos.clear(); + conexionesEnergia.clear(); + + componentes.clear(); + distribuciones.clear(); + conexiones.clear(); + conexionesConCables.clear(); + topologiaOptimizada.clear(); + componentesRows.clear(); + showDetallesEspecificos = false; + + detalleCable = null; + detalleSwitch = null; + detallePatchPanel = null; + detalleRack = null; + detalleOrganizador = null; + detalleUps = null; + detalleRouterFirewall = null; + detalleEquipoActivo = null; + } + + // MÉTODOS DE UTILIDAD PARA TOPOLOGÍA + List getComponentesPorTipoTopologia(String tipo) { + return componentesTopologia.where((c) { + if (!c.activo) return false; + + switch (tipo.toLowerCase()) { + case 'mdf': + return c.esMDF; + case 'idf': + return c.esIDF; + case 'switch': + return c.esSwitch; + case 'router': + return c.esRouter; + case 'servidor': + case 'server': + return c.esServidor; + case 'ups': + return c.esUPS; + case 'rack': + return c.esRack; + case 'patch': + case 'panel': + return c.esPatchPanel; + default: + return false; + } + }).toList() + ..sort((a, b) => a.prioridadTopologia.compareTo(b.prioridadTopologia)); + } + + List getComponentesMDF() { + return getComponentesPorTipoTopologia('mdf'); + } + + List getComponentesIDF() { + return getComponentesPorTipoTopologia('idf'); + } + + List getComponentesSwitch() { + return getComponentesPorTipoTopologia('switch'); + } + + List getComponentesRouter() { + return getComponentesPorTipoTopologia('router'); + } + + List getComponentesServidor() { + return getComponentesPorTipoTopologia('servidor'); + } + + List getConexionesPorComponente(String componenteId) { + return conexionesDatos + .where((c) => + c.componenteOrigenId == componenteId || + c.componenteDestinoId == componenteId) + .toList(); + } + + List getConexionesEnergiaPorComponente(String componenteId) { + return conexionesEnergia + .where((c) => c.origenId == componenteId || c.destinoId == componenteId) + .toList(); + } }