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 0000000..689f4d6 Binary files /dev/null and b/assets/referencia/categoria_componente.png differ 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 0000000..e15dba9 Binary files /dev/null and b/assets/referencia/rol_logico_componente.png differ 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 0000000..f1e0a48 Binary files /dev/null and b/assets/referencia/tipo_distribucion.png differ 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(); + } }