diff --git a/assets/referencia/nethive_tablas.md b/assets/referencia/nethive_tablas.md new file mode 100644 index 0000000..454fcc5 --- /dev/null +++ b/assets/referencia/nethive_tablas.md @@ -0,0 +1,182 @@ +# 📘 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() | + +--- + +## 🔌 `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 | diff --git a/assets/referencia/videos_provider.txt b/assets/referencia/videos_provider.txt new file mode 100644 index 0000000..34900e5 --- /dev/null +++ b/assets/referencia/videos_provider.txt @@ -0,0 +1,1249 @@ +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/main.dart b/lib/main.dart index befb0ae..54114e7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,6 +10,8 @@ import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:nethive_neo/providers/user_provider.dart'; import 'package:nethive_neo/providers/visual_state_provider.dart'; import 'package:nethive_neo/providers/users_provider.dart'; +import 'package:nethive_neo/providers/nethive/empresas_negocios_provider.dart'; +import 'package:nethive_neo/providers/nethive/componentes_provider.dart'; import 'package:nethive_neo/helpers/globals.dart'; import 'package:url_strategy/url_strategy.dart'; @@ -42,6 +44,8 @@ void main() async { ChangeNotifierProvider( create: (context) => VisualStateProvider(context)), ChangeNotifierProvider(create: (_) => UsersProvider()), + ChangeNotifierProvider(create: (_) => EmpresasNegociosProvider()), + ChangeNotifierProvider(create: (_) => ComponentesProvider()), ], child: const MyApp(), ), diff --git a/lib/models/nethive/categoria_componente_model.dart b/lib/models/nethive/categoria_componente_model.dart new file mode 100644 index 0000000..3767633 --- /dev/null +++ b/lib/models/nethive/categoria_componente_model.dart @@ -0,0 +1,29 @@ +import 'dart:convert'; + +class CategoriaComponente { + final int id; + final String nombre; + + CategoriaComponente({ + required this.id, + required this.nombre, + }); + + factory CategoriaComponente.fromMap(Map map) { + return CategoriaComponente( + id: map['id'], + nombre: map['nombre'], + ); + } + + Map toMap() { + return { + 'id': id, + 'nombre': nombre, + }; + } + + factory CategoriaComponente.fromJson(String source) => + CategoriaComponente.fromMap(json.decode(source)); + String toJson() => json.encode(toMap()); +} diff --git a/lib/models/nethive/componente_model.dart b/lib/models/nethive/componente_model.dart new file mode 100644 index 0000000..8847e23 --- /dev/null +++ b/lib/models/nethive/componente_model.dart @@ -0,0 +1,61 @@ +import 'dart:convert'; + +class Componente { + final String id; + final String negocioId; + final int categoriaId; + final String nombre; + final String? descripcion; + final bool enUso; + final bool activo; + final String? ubicacion; + final String? imagenUrl; + final DateTime fechaRegistro; + + Componente({ + required this.id, + required this.negocioId, + required this.categoriaId, + required this.nombre, + this.descripcion, + required this.enUso, + required this.activo, + this.ubicacion, + this.imagenUrl, + required this.fechaRegistro, + }); + + factory Componente.fromMap(Map map) { + return Componente( + id: map['id'], + negocioId: map['negocio_id'], + categoriaId: map['categoria_id'], + nombre: map['nombre'], + descripcion: map['descripcion'], + enUso: map['en_uso'], + activo: map['activo'], + ubicacion: map['ubicacion'], + imagenUrl: map['imagen_url'], + fechaRegistro: DateTime.parse(map['fecha_registro']), + ); + } + + Map toMap() { + return { + 'id': id, + 'negocio_id': negocioId, + 'categoria_id': categoriaId, + 'nombre': nombre, + 'descripcion': descripcion, + 'en_uso': enUso, + 'activo': activo, + 'ubicacion': ubicacion, + 'imagen_url': imagenUrl, + 'fecha_registro': fechaRegistro.toIso8601String(), + }; + } + + factory Componente.fromJson(String source) => + Componente.fromMap(json.decode(source)); + String toJson() => json.encode(toMap()); +} diff --git a/lib/models/nethive/detalle_cable_model.dart b/lib/models/nethive/detalle_cable_model.dart new file mode 100644 index 0000000..c3c5a5e --- /dev/null +++ b/lib/models/nethive/detalle_cable_model.dart @@ -0,0 +1,41 @@ +import 'dart:convert'; + +class DetalleCable { + final String componenteId; + final String? tipoCable; + final String? color; + final double? tamano; + final String? tipoConector; + + DetalleCable({ + required this.componenteId, + this.tipoCable, + this.color, + this.tamano, + this.tipoConector, + }); + + factory DetalleCable.fromMap(Map map) { + return DetalleCable( + componenteId: map['componente_id'], + tipoCable: map['tipo_cable'], + color: map['color'], + tamano: map['tamaño']?.toDouble(), + tipoConector: map['tipo_conector'], + ); + } + + Map toMap() { + return { + 'componente_id': componenteId, + 'tipo_cable': tipoCable, + 'color': color, + 'tamaño': tamano, + 'tipo_conector': tipoConector, + }; + } + + factory DetalleCable.fromJson(String source) => + DetalleCable.fromMap(json.decode(source)); + String toJson() => json.encode(toMap()); +} diff --git a/lib/models/nethive/detalle_equipo_activo_model.dart b/lib/models/nethive/detalle_equipo_activo_model.dart new file mode 100644 index 0000000..1fe87d0 --- /dev/null +++ b/lib/models/nethive/detalle_equipo_activo_model.dart @@ -0,0 +1,53 @@ +import 'dart:convert'; + +class DetalleEquipoActivo { + final String componenteId; + final String? tipo; + final String? marca; + final String? modelo; + final String? numeroSerie; + final String? especificaciones; + final String? direccionIp; + final String? firmware; + + DetalleEquipoActivo({ + required this.componenteId, + this.tipo, + this.marca, + this.modelo, + this.numeroSerie, + this.especificaciones, + this.direccionIp, + this.firmware, + }); + + factory DetalleEquipoActivo.fromMap(Map map) { + return DetalleEquipoActivo( + componenteId: map['componente_id'], + tipo: map['tipo'], + marca: map['marca'], + modelo: map['modelo'], + numeroSerie: map['numero_serie'], + especificaciones: map['especificaciones'], + direccionIp: map['direccion_ip'], + firmware: map['firmware'], + ); + } + + Map toMap() { + return { + 'componente_id': componenteId, + 'tipo': tipo, + 'marca': marca, + 'modelo': modelo, + 'numero_serie': numeroSerie, + 'especificaciones': especificaciones, + 'direccion_ip': direccionIp, + 'firmware': firmware, + }; + } + + factory DetalleEquipoActivo.fromJson(String source) => + DetalleEquipoActivo.fromMap(json.decode(source)); + String toJson() => json.encode(toMap()); +} diff --git a/lib/models/nethive/detalle_organizador_model.dart b/lib/models/nethive/detalle_organizador_model.dart new file mode 100644 index 0000000..15dbd5f --- /dev/null +++ b/lib/models/nethive/detalle_organizador_model.dart @@ -0,0 +1,41 @@ +import 'dart:convert'; + +class DetalleOrganizador { + final String componenteId; + final String? tipo; + final String? material; + final String? tamano; + final String? color; + + DetalleOrganizador({ + required this.componenteId, + this.tipo, + this.material, + this.tamano, + this.color, + }); + + factory DetalleOrganizador.fromMap(Map map) { + return DetalleOrganizador( + componenteId: map['componente_id'], + tipo: map['tipo'], + material: map['material'], + tamano: map['tamaño'], + color: map['color'], + ); + } + + Map toMap() { + return { + 'componente_id': componenteId, + 'tipo': tipo, + 'material': material, + 'tamaño': tamano, + 'color': color, + }; + } + + factory DetalleOrganizador.fromJson(String source) => + DetalleOrganizador.fromMap(json.decode(source)); + String toJson() => json.encode(toMap()); +} diff --git a/lib/models/nethive/detalle_patch_panel_model.dart b/lib/models/nethive/detalle_patch_panel_model.dart new file mode 100644 index 0000000..1e0b689 --- /dev/null +++ b/lib/models/nethive/detalle_patch_panel_model.dart @@ -0,0 +1,49 @@ +import 'dart:convert'; + +class DetallePatchPanel { + final String componenteId; + final String? tipoConector; + final int? numeroPuertos; + final String? categoria; + final String? tipoMontaje; + final bool? numeracionFrontal; + final bool? panelCiego; + + DetallePatchPanel({ + required this.componenteId, + this.tipoConector, + this.numeroPuertos, + this.categoria, + this.tipoMontaje, + this.numeracionFrontal, + this.panelCiego, + }); + + factory DetallePatchPanel.fromMap(Map map) { + return DetallePatchPanel( + componenteId: map['componente_id'], + tipoConector: map['tipo_conector'], + numeroPuertos: map['numero_puertos'], + categoria: map['categoria'], + tipoMontaje: map['tipo_montaje'], + numeracionFrontal: map['numeracion_frontal'], + panelCiego: map['panel_ciego'], + ); + } + + Map toMap() { + return { + 'componente_id': componenteId, + 'tipo_conector': tipoConector, + 'numero_puertos': numeroPuertos, + 'categoria': categoria, + 'tipo_montaje': tipoMontaje, + 'numeracion_frontal': numeracionFrontal, + 'panel_ciego': panelCiego, + }; + } + + factory DetallePatchPanel.fromJson(String source) => + DetallePatchPanel.fromMap(json.decode(source)); + String toJson() => json.encode(toMap()); +} diff --git a/lib/models/nethive/detalle_rack_model.dart b/lib/models/nethive/detalle_rack_model.dart new file mode 100644 index 0000000..bf449f3 --- /dev/null +++ b/lib/models/nethive/detalle_rack_model.dart @@ -0,0 +1,57 @@ +import 'dart:convert'; + +class DetalleRack { + final String componenteId; + final String? tipo; + final int? alturaU; + final int? profundidadCm; + final int? anchoCm; + final bool? ventilacionIntegrada; + final bool? puertasConLlave; + final bool? ruedas; + final String? color; + + DetalleRack({ + required this.componenteId, + this.tipo, + this.alturaU, + this.profundidadCm, + this.anchoCm, + this.ventilacionIntegrada, + this.puertasConLlave, + this.ruedas, + this.color, + }); + + factory DetalleRack.fromMap(Map map) { + return DetalleRack( + componenteId: map['componente_id'], + tipo: map['tipo'], + alturaU: map['altura_u'], + profundidadCm: map['profundidad_cm'], + anchoCm: map['ancho_cm'], + ventilacionIntegrada: map['ventilacion_integrada'], + puertasConLlave: map['puertas_con_llave'], + ruedas: map['ruedas'], + color: map['color'], + ); + } + + Map toMap() { + return { + 'componente_id': componenteId, + 'tipo': tipo, + 'altura_u': alturaU, + 'profundidad_cm': profundidadCm, + 'ancho_cm': anchoCm, + 'ventilacion_integrada': ventilacionIntegrada, + 'puertas_con_llave': puertasConLlave, + 'ruedas': ruedas, + 'color': color, + }; + } + + factory DetalleRack.fromJson(String source) => + DetalleRack.fromMap(json.decode(source)); + String toJson() => json.encode(toMap()); +} diff --git a/lib/models/nethive/detalle_router_firewall_model.dart b/lib/models/nethive/detalle_router_firewall_model.dart new file mode 100644 index 0000000..75b4601 --- /dev/null +++ b/lib/models/nethive/detalle_router_firewall_model.dart @@ -0,0 +1,61 @@ +import 'dart:convert'; + +class DetalleRouterFirewall { + final String componenteId; + final String? tipo; + final String? marca; + final String? modelo; + final String? numeroSerie; + final String? interfaces; + final double? capacidadRoutingGbps; + final String? direccionIp; + final String? firmware; + final String? licencias; + + DetalleRouterFirewall({ + required this.componenteId, + this.tipo, + this.marca, + this.modelo, + this.numeroSerie, + this.interfaces, + this.capacidadRoutingGbps, + this.direccionIp, + this.firmware, + this.licencias, + }); + + factory DetalleRouterFirewall.fromMap(Map map) { + return DetalleRouterFirewall( + componenteId: map['componente_id'], + tipo: map['tipo'], + marca: map['marca'], + modelo: map['modelo'], + numeroSerie: map['numero_serie'], + interfaces: map['interfaces'], + capacidadRoutingGbps: map['capacidad_routing_gbps']?.toDouble(), + direccionIp: map['direccion_ip'], + firmware: map['firmware'], + licencias: map['licencias'], + ); + } + + Map toMap() { + return { + 'componente_id': componenteId, + 'tipo': tipo, + 'marca': marca, + 'modelo': modelo, + 'numero_serie': numeroSerie, + 'interfaces': interfaces, + 'capacidad_routing_gbps': capacidadRoutingGbps, + 'direccion_ip': direccionIp, + 'firmware': firmware, + 'licencias': licencias, + }; + } + + factory DetalleRouterFirewall.fromJson(String source) => + DetalleRouterFirewall.fromMap(json.decode(source)); + String toJson() => json.encode(toMap()); +} diff --git a/lib/models/nethive/detalle_switch_model.dart b/lib/models/nethive/detalle_switch_model.dart new file mode 100644 index 0000000..2b1b6ce --- /dev/null +++ b/lib/models/nethive/detalle_switch_model.dart @@ -0,0 +1,69 @@ +import 'dart:convert'; + +class DetalleSwitch { + final String componenteId; + final String? marca; + final String? modelo; + final String? numeroSerie; + final bool? administrable; + final bool? poe; + final int? cantidadPuertos; + final String? velocidadPuertos; + final String? tipoPuertos; + final String? ubicacionEnRack; + final String? direccionIp; + final String? firmware; + + DetalleSwitch({ + required this.componenteId, + this.marca, + this.modelo, + this.numeroSerie, + this.administrable, + this.poe, + this.cantidadPuertos, + this.velocidadPuertos, + this.tipoPuertos, + this.ubicacionEnRack, + this.direccionIp, + this.firmware, + }); + + factory DetalleSwitch.fromMap(Map map) { + return DetalleSwitch( + componenteId: map['componente_id'], + marca: map['marca'], + modelo: map['modelo'], + numeroSerie: map['numero_serie'], + administrable: map['administrable'], + poe: map['poe'], + cantidadPuertos: map['cantidad_puertos'], + velocidadPuertos: map['velocidad_puertos'], + tipoPuertos: map['tipo_puertos'], + ubicacionEnRack: map['ubicacion_en_rack'], + direccionIp: map['direccion_ip'], + firmware: map['firmware'], + ); + } + + Map toMap() { + return { + 'componente_id': componenteId, + 'marca': marca, + 'modelo': modelo, + 'numero_serie': numeroSerie, + 'administrable': administrable, + 'poe': poe, + 'cantidad_puertos': cantidadPuertos, + 'velocidad_puertos': velocidadPuertos, + 'tipo_puertos': tipoPuertos, + 'ubicacion_en_rack': ubicacionEnRack, + 'direccion_ip': direccionIp, + 'firmware': firmware, + }; + } + + factory DetalleSwitch.fromJson(String source) => + DetalleSwitch.fromMap(json.decode(source)); + String toJson() => json.encode(toMap()); +} diff --git a/lib/models/nethive/detalle_ups_model.dart b/lib/models/nethive/detalle_ups_model.dart new file mode 100644 index 0000000..423971c --- /dev/null +++ b/lib/models/nethive/detalle_ups_model.dart @@ -0,0 +1,61 @@ +import 'dart:convert'; + +class DetalleUps { + final String componenteId; + final String? tipo; + final String? marca; + final String? modelo; + final String? voltajeEntrada; + final String? voltajeSalida; + final int? capacidadVa; + final int? autonomiaMinutos; + final int? cantidadTomas; + final bool? rackeable; + + DetalleUps({ + required this.componenteId, + this.tipo, + this.marca, + this.modelo, + this.voltajeEntrada, + this.voltajeSalida, + this.capacidadVa, + this.autonomiaMinutos, + this.cantidadTomas, + this.rackeable, + }); + + factory DetalleUps.fromMap(Map map) { + return DetalleUps( + componenteId: map['componente_id'], + tipo: map['tipo'], + marca: map['marca'], + modelo: map['modelo'], + voltajeEntrada: map['voltaje_entrada'], + voltajeSalida: map['voltaje_salida'], + capacidadVa: map['capacidad_va'], + autonomiaMinutos: map['autonomia_minutos'], + cantidadTomas: map['cantidad_tomas'], + rackeable: map['rackeable'], + ); + } + + Map toMap() { + return { + 'componente_id': componenteId, + 'tipo': tipo, + 'marca': marca, + 'modelo': modelo, + 'voltaje_entrada': voltajeEntrada, + 'voltaje_salida': voltajeSalida, + 'capacidad_va': capacidadVa, + 'autonomia_minutos': autonomiaMinutos, + 'cantidad_tomas': cantidadTomas, + 'rackeable': rackeable, + }; + } + + factory DetalleUps.fromJson(String source) => + DetalleUps.fromMap(json.decode(source)); + String toJson() => json.encode(toMap()); +} diff --git a/lib/models/nethive/empresa_model.dart b/lib/models/nethive/empresa_model.dart new file mode 100644 index 0000000..973300c --- /dev/null +++ b/lib/models/nethive/empresa_model.dart @@ -0,0 +1,57 @@ +import 'dart:convert'; + +class Empresa { + final String id; + final String nombre; + final String rfc; + final String direccion; + final String telefono; + final String email; + final DateTime fechaCreacion; + final String? logoUrl; + final String? imagenUrl; + + Empresa({ + required this.id, + required this.nombre, + required this.rfc, + required this.direccion, + required this.telefono, + required this.email, + required this.fechaCreacion, + this.logoUrl, + this.imagenUrl, + }); + + factory Empresa.fromMap(Map map) { + return Empresa( + id: map['id'], + nombre: map['nombre'], + rfc: map['rfc'], + direccion: map['direccion'], + telefono: map['telefono'], + email: map['email'], + fechaCreacion: DateTime.parse(map['fecha_creacion']), + logoUrl: map['logo_url'], + imagenUrl: map['imagen_url'], + ); + } + + Map toMap() { + return { + 'id': id, + 'nombre': nombre, + 'rfc': rfc, + 'direccion': direccion, + 'telefono': telefono, + 'email': email, + 'fecha_creacion': fechaCreacion.toIso8601String(), + 'logo_url': logoUrl, + 'imagen_url': imagenUrl, + }; + } + + factory Empresa.fromJson(String source) => + Empresa.fromMap(json.decode(source)); + String toJson() => json.encode(toMap()); +} diff --git a/lib/models/nethive/negocio_model.dart b/lib/models/nethive/negocio_model.dart new file mode 100644 index 0000000..8a37520 --- /dev/null +++ b/lib/models/nethive/negocio_model.dart @@ -0,0 +1,61 @@ +import 'dart:convert'; + +class Negocio { + final String id; + final String empresaId; + final String nombre; + final String direccion; + final double latitud; + final double longitud; + final String tipoLocal; + final DateTime fechaCreacion; + final String? logoUrl; + final String? imagenUrl; + + Negocio({ + required this.id, + required this.empresaId, + required this.nombre, + required this.direccion, + required this.latitud, + required this.longitud, + required this.tipoLocal, + required this.fechaCreacion, + this.logoUrl, + this.imagenUrl, + }); + + factory Negocio.fromMap(Map map) { + return Negocio( + id: map['id'], + empresaId: map['empresa_id'], + nombre: map['nombre'], + direccion: map['direccion'], + latitud: map['latitud'].toDouble(), + longitud: map['longitud'].toDouble(), + tipoLocal: map['tipo_local'], + fechaCreacion: DateTime.parse(map['fecha_creacion']), + logoUrl: map['logo_url'], + imagenUrl: map['imagen_url'], + ); + } + + Map toMap() { + return { + 'id': id, + 'empresa_id': empresaId, + 'nombre': nombre, + 'direccion': direccion, + 'latitud': latitud, + 'longitud': longitud, + 'tipo_local': tipoLocal, + 'fecha_creacion': fechaCreacion.toIso8601String(), + 'logo_url': logoUrl, + 'imagen_url': imagenUrl, + }; + } + + factory Negocio.fromJson(String source) => + Negocio.fromMap(json.decode(source)); + String toJson() => json.encode(toMap()); +} diff --git a/lib/providers/nethive/componentes_provider.dart b/lib/providers/nethive/componentes_provider.dart new file mode 100644 index 0000000..7d538ec --- /dev/null +++ b/lib/providers/nethive/componentes_provider.dart @@ -0,0 +1,533 @@ +import 'dart:convert'; +import 'dart:typed_data'; +import 'package:file_picker/file_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:nethive_neo/helpers/globals.dart'; +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/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'; +import 'package:nethive_neo/models/nethive/detalle_rack_model.dart'; +import 'package:nethive_neo/models/nethive/detalle_organizador_model.dart'; +import 'package:nethive_neo/models/nethive/detalle_ups_model.dart'; +import 'package:nethive_neo/models/nethive/detalle_router_firewall_model.dart'; +import 'package:nethive_neo/models/nethive/detalle_equipo_activo_model.dart'; + +class ComponentesProvider extends ChangeNotifier { + // State managers + PlutoGridStateManager? componentesStateManager; + PlutoGridStateManager? categoriasStateManager; + + // Controladores de búsqueda + final busquedaComponenteController = TextEditingController(); + final busquedaCategoriaController = TextEditingController(); + + // Listas principales + List categorias = []; + List componentes = []; + List componentesRows = []; + List categoriasRows = []; + + // Variables para formularios + String? imagenFileName; + Uint8List? imagenToUpload; + String? negocioSeleccionadoId; + int? categoriaSeleccionadaId; + bool showDetallesEspecificos = false; + + // Detalles específicos por tipo de componente + DetalleCable? detalleCable; + DetalleSwitch? detalleSwitch; + DetallePatchPanel? detallePatchPanel; + DetalleRack? detalleRack; + DetalleOrganizador? detalleOrganizador; + DetalleUps? detalleUps; + DetalleRouterFirewall? detalleRouterFirewall; + DetalleEquipoActivo? detalleEquipoActivo; + + ComponentesProvider() { + getCategorias(); + } + + // Métodos para categorías + Future getCategorias([String? busqueda]) async { + try { + var query = supabaseLU.from('categoria_componente').select(); + + if (busqueda != null && busqueda.isNotEmpty) { + query = query.ilike('nombre', '%$busqueda%'); + } + + final res = await query.order('nombre', ascending: true); + + categorias = (res as List) + .map((categoria) => CategoriaComponente.fromMap(categoria)) + .toList(); + + _buildCategoriasRows(); + notifyListeners(); + } catch (e) { + print('Error en getCategorias: ${e.toString()}'); + } + } + + void _buildCategoriasRows() { + categoriasRows.clear(); + + for (CategoriaComponente categoria in categorias) { + categoriasRows.add(PlutoRow(cells: { + 'id': PlutoCell(value: categoria.id), + 'nombre': PlutoCell(value: categoria.nombre), + 'editar': PlutoCell(value: categoria.id), + 'eliminar': PlutoCell(value: categoria.id), + })); + } + } + + // Métodos para componentes + Future getComponentesPorNegocio(String negocioId, + [String? busqueda]) async { + try { + var query = supabaseLU.from('componente').select(''' + *, + categoria_componente!inner(id, nombre) + ''').eq('negocio_id', negocioId); + + if (busqueda != null && busqueda.isNotEmpty) { + query = query.or( + 'nombre.ilike.%$busqueda%,descripcion.ilike.%$busqueda%,ubicacion.ilike.%$busqueda%'); + } + + final res = await query.order('fecha_registro', ascending: false); + + componentes = (res as List) + .map((componente) => Componente.fromMap(componente)) + .toList(); + + _buildComponentesRows(); + notifyListeners(); + } catch (e) { + print('Error en getComponentesPorNegocio: ${e.toString()}'); + } + } + + void _buildComponentesRows() { + componentesRows.clear(); + + for (Componente componente in componentes) { + componentesRows.add(PlutoRow(cells: { + 'id': PlutoCell(value: componente.id), + 'negocio_id': PlutoCell(value: componente.negocioId), + 'categoria_id': PlutoCell(value: componente.categoriaId), + 'categoria_nombre': PlutoCell( + value: getCategoriaById(componente.categoriaId)?.nombre ?? + 'Sin categoría'), + 'nombre': PlutoCell(value: componente.nombre), + 'descripcion': PlutoCell(value: componente.descripcion ?? ''), + 'en_uso': PlutoCell(value: componente.enUso ? 'Sí' : 'No'), + 'activo': PlutoCell(value: componente.activo ? 'Sí' : 'No'), + 'ubicacion': PlutoCell(value: componente.ubicacion ?? ''), + 'fecha_registro': + PlutoCell(value: componente.fechaRegistro.toString().split(' ')[0]), + 'imagen_url': PlutoCell( + value: componente.imagenUrl != null + ? "${supabaseLU.supabaseUrl}/storage/v1/object/public/nethive/componentes/${componente.imagenUrl}?${DateTime.now().millisecondsSinceEpoch}" + : '', + ), + 'editar': PlutoCell(value: componente.id), + 'eliminar': PlutoCell(value: componente.id), + 'ver_detalles': PlutoCell(value: componente.id), + })); + } + } + + // Métodos para subir 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; + } + + notifyListeners(); + } + + 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; + } + + // CRUD Categorías + 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; + } + } + + // CRUD Componentes + 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 eliminarComponente(String componenteId) async { + try { + // Eliminar todos los detalles específicos primero + await _eliminarDetallesComponente(componenteId); + + // Luego eliminar el componente + await supabaseLU.from('componente').delete().eq('id', componenteId); + + if (negocioSeleccionadoId != null) { + await getComponentesPorNegocio(negocioSeleccionadoId!); + } + return true; + } catch (e) { + print('Error en eliminarComponente: ${e.toString()}'); + return false; + } + } + + Future _eliminarDetallesComponente(String componenteId) async { + // Eliminar de todas las tablas de detalles + await supabaseLU + .from('detalle_cable') + .delete() + .eq('componente_id', componenteId); + await supabaseLU + .from('detalle_switch') + .delete() + .eq('componente_id', componenteId); + await supabaseLU + .from('detalle_patch_panel') + .delete() + .eq('componente_id', componenteId); + await supabaseLU + .from('detalle_rack') + .delete() + .eq('componente_id', componenteId); + await supabaseLU + .from('detalle_organizador') + .delete() + .eq('componente_id', componenteId); + await supabaseLU + .from('detalle_ups') + .delete() + .eq('componente_id', componenteId); + await supabaseLU + .from('detalle_router_firewall') + .delete() + .eq('componente_id', componenteId); + await supabaseLU + .from('detalle_equipo_activo') + .delete() + .eq('componente_id', componenteId); + } + + // Métodos para obtener detalles específicos + Future getDetallesComponente( + String componenteId, int categoriaId) async { + try { + final categoriaNombre = + getCategoriaById(categoriaId)?.nombre?.toLowerCase() ?? ''; + + if (categoriaNombre.contains('cable')) { + await _getDetalleCable(componenteId); + } else if (categoriaNombre.contains('switch')) { + await _getDetalleSwitch(componenteId); + } else if (categoriaNombre.contains('patch') || + categoriaNombre.contains('panel')) { + await _getDetallePatchPanel(componenteId); + } else if (categoriaNombre.contains('rack')) { + await _getDetalleRack(componenteId); + } else if (categoriaNombre.contains('organizador')) { + await _getDetalleOrganizador(componenteId); + } else if (categoriaNombre.contains('ups')) { + await _getDetalleUps(componenteId); + } else if (categoriaNombre.contains('router') || + categoriaNombre.contains('firewall')) { + await _getDetalleRouterFirewall(componenteId); + } else { + await _getDetalleEquipoActivo(componenteId); + } + + showDetallesEspecificos = true; + notifyListeners(); + } catch (e) { + print('Error en getDetallesComponente: ${e.toString()}'); + } + } + + Future _getDetalleCable(String componenteId) async { + final res = await supabaseLU + .from('detalle_cable') + .select() + .eq('componente_id', componenteId) + .maybeSingle(); + + if (res != null) { + detalleCable = DetalleCable.fromMap(res); + } + } + + Future _getDetalleSwitch(String componenteId) async { + final res = await supabaseLU + .from('detalle_switch') + .select() + .eq('componente_id', componenteId) + .maybeSingle(); + + if (res != null) { + detalleSwitch = DetalleSwitch.fromMap(res); + } + } + + Future _getDetallePatchPanel(String componenteId) async { + final res = await supabaseLU + .from('detalle_patch_panel') + .select() + .eq('componente_id', componenteId) + .maybeSingle(); + + if (res != null) { + detallePatchPanel = DetallePatchPanel.fromMap(res); + } + } + + Future _getDetalleRack(String componenteId) async { + final res = await supabaseLU + .from('detalle_rack') + .select() + .eq('componente_id', componenteId) + .maybeSingle(); + + if (res != null) { + detalleRack = DetalleRack.fromMap(res); + } + } + + Future _getDetalleOrganizador(String componenteId) async { + final res = await supabaseLU + .from('detalle_organizador') + .select() + .eq('componente_id', componenteId) + .maybeSingle(); + + if (res != null) { + detalleOrganizador = DetalleOrganizador.fromMap(res); + } + } + + Future _getDetalleUps(String componenteId) async { + final res = await supabaseLU + .from('detalle_ups') + .select() + .eq('componente_id', componenteId) + .maybeSingle(); + + if (res != null) { + detalleUps = DetalleUps.fromMap(res); + } + } + + Future _getDetalleRouterFirewall(String componenteId) async { + final res = await supabaseLU + .from('detalle_router_firewall') + .select() + .eq('componente_id', componenteId) + .maybeSingle(); + + if (res != null) { + detalleRouterFirewall = DetalleRouterFirewall.fromMap(res); + } + } + + Future _getDetalleEquipoActivo(String componenteId) async { + final res = await supabaseLU + .from('detalle_equipo_activo') + .select() + .eq('componente_id', componenteId) + .maybeSingle(); + + if (res != null) { + detalleEquipoActivo = DetalleEquipoActivo.fromMap(res); + } + } + + // Métodos de utilidad + void setNegocioSeleccionado(String negocioId) { + negocioSeleccionadoId = negocioId; + getComponentesPorNegocio(negocioId); + notifyListeners(); + } + + CategoriaComponente? getCategoriaById(int id) { + try { + return categorias.firstWhere((c) => c.id == id); + } catch (e) { + return null; + } + } + + void resetFormData() { + imagenFileName = null; + imagenToUpload = null; + categoriaSeleccionadaId = null; + showDetallesEspecificos = false; + + // Limpiar detalles específicos + detalleCable = null; + detalleSwitch = null; + detallePatchPanel = null; + detalleRack = null; + detalleOrganizador = null; + detalleUps = null; + detalleRouterFirewall = null; + detalleEquipoActivo = null; + + notifyListeners(); + } + + void buscarComponentes(String busqueda) { + if (negocioSeleccionadoId != null) { + getComponentesPorNegocio( + negocioSeleccionadoId!, busqueda.isEmpty ? null : busqueda); + } + } + + void buscarCategorias(String busqueda) { + getCategorias(busqueda.isEmpty ? null : busqueda); + } + + Widget? getImageWidget(dynamic image, + {double height = 100, double width = 100}) { + if (image == null || image.toString().isEmpty) { + return Container( + height: height, + width: width, + color: Colors.grey[300], + child: const Icon(Icons.device_unknown), + ); + } else if (image is Uint8List) { + return Image.memory( + image, + height: height, + width: width, + fit: BoxFit.cover, + ); + } else if (image is String) { + return Image.network( + image, + height: height, + width: width, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Container( + height: height, + width: width, + color: Colors.grey[300], + child: const Icon(Icons.broken_image), + ); + }, + ); + } + return null; + } +} diff --git a/lib/providers/nethive/empresas_negocios_provider.dart b/lib/providers/nethive/empresas_negocios_provider.dart new file mode 100644 index 0000000..b5e63f3 --- /dev/null +++ b/lib/providers/nethive/empresas_negocios_provider.dart @@ -0,0 +1,371 @@ +import 'dart:convert'; +import 'dart:typed_data'; +import 'package:file_picker/file_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:nethive_neo/helpers/globals.dart'; +import 'package:nethive_neo/models/nethive/empresa_model.dart'; +import 'package:nethive_neo/models/nethive/negocio_model.dart'; + +class EmpresasNegociosProvider extends ChangeNotifier { + // State managers para las grillas + PlutoGridStateManager? empresasStateManager; + PlutoGridStateManager? negociosStateManager; + + // Controladores de búsqueda + final busquedaEmpresaController = TextEditingController(); + final busquedaNegocioController = TextEditingController(); + + // Listas de datos + List empresas = []; + List negocios = []; + List empresasRows = []; + List negociosRows = []; + + // Variables para formularios + String? logoFileName; + String? imagenFileName; + Uint8List? logoToUpload; + Uint8List? imagenToUpload; + + // Variables de selección + String? empresaSeleccionadaId; + Empresa? empresaSeleccionada; + + EmpresasNegociosProvider() { + getEmpresas(); + } + + // Métodos para empresas + Future getEmpresas([String? busqueda]) async { + try { + var query = supabaseLU.from('empresa').select(); + + if (busqueda != null && busqueda.isNotEmpty) { + query = query.or( + 'nombre.ilike.%$busqueda%,rfc.ilike.%$busqueda%,email.ilike.%$busqueda%'); + } + + final res = await query.order('fecha_creacion', ascending: false); + + empresas = (res as List) + .map((empresa) => Empresa.fromMap(empresa)) + .toList(); + + _buildEmpresasRows(); + notifyListeners(); + } catch (e) { + print('Error en getEmpresas: ${e.toString()}'); + } + } + + void _buildEmpresasRows() { + empresasRows.clear(); + + for (Empresa empresa in empresas) { + empresasRows.add(PlutoRow(cells: { + 'id': PlutoCell(value: empresa.id), + 'nombre': PlutoCell(value: empresa.nombre), + 'rfc': PlutoCell(value: empresa.rfc), + 'direccion': PlutoCell(value: empresa.direccion), + 'telefono': PlutoCell(value: empresa.telefono), + 'email': PlutoCell(value: empresa.email), + 'fecha_creacion': + PlutoCell(value: empresa.fechaCreacion.toString().split(' ')[0]), + 'logo_url': PlutoCell( + value: empresa.logoUrl != null + ? "${supabaseLU.supabaseUrl}/storage/v1/object/public/nethive/logos/${empresa.logoUrl}?${DateTime.now().millisecondsSinceEpoch}" + : '', + ), + 'imagen_url': PlutoCell( + value: empresa.imagenUrl != null + ? "${supabaseLU.supabaseUrl}/storage/v1/object/public/nethive/imagenes/${empresa.imagenUrl}?${DateTime.now().millisecondsSinceEpoch}" + : '', + ), + 'editar': PlutoCell(value: empresa.id), + 'eliminar': PlutoCell(value: empresa.id), + 'ver_negocios': PlutoCell(value: empresa.id), + })); + } + } + + Future getNegociosPorEmpresa(String empresaId) async { + try { + final res = await supabaseLU + .from('negocio') + .select() + .eq('empresa_id', empresaId) + .order('fecha_creacion', ascending: false); + + negocios = (res as List) + .map((negocio) => Negocio.fromMap(negocio)) + .toList(); + + _buildNegociosRows(); + notifyListeners(); + } catch (e) { + print('Error en getNegociosPorEmpresa: ${e.toString()}'); + } + } + + void _buildNegociosRows() { + negociosRows.clear(); + + for (Negocio negocio in negocios) { + negociosRows.add(PlutoRow(cells: { + 'id': PlutoCell(value: negocio.id), + 'empresa_id': PlutoCell(value: negocio.empresaId), + 'nombre': PlutoCell(value: negocio.nombre), + 'direccion': PlutoCell(value: negocio.direccion), + 'latitud': PlutoCell(value: negocio.latitud.toString()), + 'longitud': PlutoCell(value: negocio.longitud.toString()), + 'tipo_local': PlutoCell(value: negocio.tipoLocal), + 'fecha_creacion': + PlutoCell(value: negocio.fechaCreacion.toString().split(' ')[0]), + 'logo_url': PlutoCell( + value: negocio.logoUrl != null + ? "${supabaseLU.supabaseUrl}/storage/v1/object/public/nethive/logos/${negocio.logoUrl}?${DateTime.now().millisecondsSinceEpoch}" + : '', + ), + 'imagen_url': PlutoCell( + value: negocio.imagenUrl != null + ? "${supabaseLU.supabaseUrl}/storage/v1/object/public/nethive/imagenes/${negocio.imagenUrl}?${DateTime.now().millisecondsSinceEpoch}" + : '', + ), + 'editar': PlutoCell(value: negocio.id), + 'eliminar': PlutoCell(value: negocio.id), + 'ver_componentes': PlutoCell(value: negocio.id), + })); + } + } + + // Métodos para subir archivos + Future selectLogo() async { + logoFileName = null; + logoToUpload = 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); + + logoFileName = 'logo-$timestamp-${picker.files.single.name}'; + logoToUpload = picker.files.single.bytes; + } + + notifyListeners(); + } + + 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 = 'imagen-$timestamp-${picker.files.single.name}'; + imagenToUpload = picker.files.single.bytes; + } + + notifyListeners(); + } + + Future uploadLogo() async { + if (logoToUpload != null && logoFileName != null) { + await supabaseLU.storage.from('nethive/logos').uploadBinary( + logoFileName!, + logoToUpload!, + fileOptions: const FileOptions( + cacheControl: '3600', + upsert: false, + ), + ); + return logoFileName; + } + return null; + } + + Future uploadImagen() async { + if (imagenToUpload != null && imagenFileName != null) { + await supabaseLU.storage.from('nethive/imagenes').uploadBinary( + imagenFileName!, + imagenToUpload!, + fileOptions: const FileOptions( + cacheControl: '3600', + upsert: false, + ), + ); + return imagenFileName; + } + return null; + } + + // CRUD Empresas + Future crearEmpresa({ + required String nombre, + required String rfc, + required String direccion, + required String telefono, + required String email, + }) async { + try { + final logoUrl = await uploadLogo(); + final imagenUrl = await uploadImagen(); + + final res = await supabaseLU.from('empresa').insert({ + 'nombre': nombre, + 'rfc': rfc, + 'direccion': direccion, + 'telefono': telefono, + 'email': email, + 'logo_url': logoUrl, + 'imagen_url': imagenUrl, + }).select(); + + if (res.isNotEmpty) { + await getEmpresas(); + resetFormData(); + return true; + } + return false; + } catch (e) { + print('Error en crearEmpresa: ${e.toString()}'); + return false; + } + } + + Future crearNegocio({ + required String empresaId, + required String nombre, + required String direccion, + required double latitud, + required double longitud, + required String tipoLocal, + }) async { + try { + final logoUrl = await uploadLogo(); + final imagenUrl = await uploadImagen(); + + final res = await supabaseLU.from('negocio').insert({ + 'empresa_id': empresaId, + 'nombre': nombre, + 'direccion': direccion, + 'latitud': latitud, + 'longitud': longitud, + 'tipo_local': tipoLocal, + 'logo_url': logoUrl, + 'imagen_url': imagenUrl, + }).select(); + + if (res.isNotEmpty) { + await getNegociosPorEmpresa(empresaId); + resetFormData(); + return true; + } + return false; + } catch (e) { + print('Error en crearNegocio: ${e.toString()}'); + return false; + } + } + + Future eliminarEmpresa(String empresaId) async { + try { + // Primero eliminar todos los negocios asociados + await supabaseLU.from('negocio').delete().eq('empresa_id', empresaId); + + // Luego eliminar la empresa + await supabaseLU.from('empresa').delete().eq('id', empresaId); + + await getEmpresas(); + return true; + } catch (e) { + print('Error en eliminarEmpresa: ${e.toString()}'); + return false; + } + } + + Future eliminarNegocio(String negocioId) async { + try { + await supabaseLU.from('negocio').delete().eq('id', negocioId); + + if (empresaSeleccionadaId != null) { + await getNegociosPorEmpresa(empresaSeleccionadaId!); + } + return true; + } catch (e) { + print('Error en eliminarNegocio: ${e.toString()}'); + return false; + } + } + + // Métodos de utilidad + void setEmpresaSeleccionada(String empresaId) { + empresaSeleccionadaId = empresaId; + empresaSeleccionada = empresas.firstWhere((e) => e.id == empresaId); + getNegociosPorEmpresa(empresaId); + notifyListeners(); + } + + void resetFormData() { + logoFileName = null; + imagenFileName = null; + logoToUpload = null; + imagenToUpload = null; + notifyListeners(); + } + + void buscarEmpresas(String busqueda) { + getEmpresas(busqueda.isEmpty ? null : busqueda); + } + + Widget? getImageWidget(dynamic image, + {double height = 100, double width = 100}) { + if (image == null || image.toString().isEmpty) { + return Container( + height: height, + width: width, + color: Colors.grey[300], + child: const Icon(Icons.image_not_supported), + ); + } else if (image is Uint8List) { + return Image.memory( + image, + height: height, + width: width, + fit: BoxFit.cover, + ); + } else if (image is String) { + return Image.network( + image, + height: height, + width: width, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Container( + height: height, + width: width, + color: Colors.grey[300], + child: const Icon(Icons.broken_image), + ); + }, + ); + } + return null; + } +} diff --git a/lib/theme/theme.dart b/lib/theme/theme.dart index c92c611..ffcbe38 100644 --- a/lib/theme/theme.dart +++ b/lib/theme/theme.dart @@ -65,23 +65,47 @@ abstract class AppTheme { abstract Color formBackground; Gradient blueGradient = const LinearGradient( - begin: Alignment.topCenter, - end: Alignment(4, 0.8), + begin: Alignment.topLeft, + end: Alignment.bottomRight, colors: [ - Color(0xFF0090FF), - Color(0xFF0363C8), - Color(0xFF063E9B), - Color(0xFF0A0859), + Color(0xFF1E40AF), // Azul profundo + Color(0xFF3B82F6), // Azul brillante + Color(0xFF0369A1), // Azul medio + Color(0xFF0F172A), // Azul muy oscuro ], ); Gradient primaryGradient = const LinearGradient( - begin: Alignment.topCenter, - end: Alignment(4, 0.8), + begin: Alignment.topLeft, + end: Alignment.bottomRight, colors: [ - Color(0xFF6C5DD3), - Color(0xFF090046), - Color(0xFF05002A), + Color(0xFF10B981), // Verde esmeralda + Color(0xFF059669), // Verde intenso + Color(0xFF0D9488), // Verde-azulado + Color(0xFF0F172A), // Azul muy oscuro + ], + ); + + // Nuevo gradiente para elementos modernos + Gradient modernGradient = const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xFF1E40AF), // Azul profundo + Color(0xFF3B82F6), // Azul brillante + Color(0xFF10B981), // Verde esmeralda + Color(0xFF7C3AED), // Púrpura + ], + ); + + // Gradiente para backgrounds oscuros + Gradient darkBackgroundGradient = const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0xFF0F172A), // Azul muy oscuro + Color(0xFF1E293B), // Azul oscuro + Color(0xFF334155), // Azul gris ], ); @@ -111,37 +135,39 @@ abstract class AppTheme { class LightModeTheme extends AppTheme { @override - Color primaryColor = const Color(0xFF663DD9); + Color primaryColor = const Color(0xFF3B82F6); // Azul brillante del login @override - Color secondaryColor = const Color(0xFF12142D); + Color secondaryColor = const Color(0xFF10B981); // Verde esmeralda del login @override - Color tertiaryColor = const Color(0xFF14B095); + Color tertiaryColor = const Color(0xFF0369A1); // Azul medio del login @override - Color alternate = const Color(0xFFFECE05); + Color alternate = const Color(0xFF7C3AED); // Púrpura del login @override Color primaryBackground = const Color(0xFFFFFFFF); @override - Color secondaryBackground = const Color(0xFFF9F9F9); + Color secondaryBackground = const Color(0xFFF8FAFC); // Gris muy claro @override - Color tertiaryBackground = const Color(0XFFF1F0F0); + Color tertiaryBackground = const Color(0xFFF1F5F9); // Gris claro azulado @override - Color transparentBackground = const Color(0XFF4D4D4D).withOpacity(.2); + Color transparentBackground = + const Color(0xFF1E293B).withOpacity(.1); // Azul oscuro transparente @override - Color primaryText = const Color(0xFF12142D); + Color primaryText = const Color(0xFF0F172A); // Azul muy oscuro del login @override - Color secondaryText = const Color(0XFF000000); + Color secondaryText = const Color(0xFF1E293B); // Azul oscuro @override - Color tertiaryText = const Color(0XFF747474); + Color tertiaryText = const Color(0xFF64748B); // Gris azulado @override - Color hintText = const Color(0XFF8A88A0); + Color hintText = const Color(0xFF94A3B8); // Gris claro @override - Color error = const Color(0XFFF44A49); + Color error = const Color(0xFFEF4444); // Rojo moderno @override - Color warning = const Color(0XFFF5AB1A); + Color warning = const Color(0xFFF59E0B); // Amarillo moderno @override - Color success = const Color(0XFF3AC170); + Color success = const Color(0xFF10B981); // Verde esmeralda del login @override - Color formBackground = const Color(0xFF663DD9).withOpacity(.2); + Color formBackground = + const Color(0xFF3B82F6).withOpacity(.05); // Azul muy claro LightModeTheme({Mode? mode}) { if (mode != null) { @@ -156,37 +182,40 @@ class LightModeTheme extends AppTheme { class DarkModeTheme extends AppTheme { @override - Color primaryColor = const Color(0xFF6C5DD3); + Color primaryColor = const Color(0xFF3B82F6); // Azul brillante del login @override - Color secondaryColor = const Color(0xFF098BF7); + Color secondaryColor = const Color(0xFF10B981); // Verde esmeralda del login @override - Color tertiaryColor = const Color(0xFF14B095); + Color tertiaryColor = const Color(0xFF0369A1); // Azul medio del login @override - Color alternate = const Color(0xFFFECE05); + Color alternate = const Color(0xFF7C3AED); // Púrpura del login @override - Color primaryBackground = Color(0xFF292929); + Color primaryBackground = + const Color(0xFF0F172A); // Azul muy oscuro del login @override - Color secondaryBackground = Color(0xFF414141); + Color secondaryBackground = const Color(0xFF1E293B); // Azul oscuro del login @override - Color tertiaryBackground = Color(0xFF343434); + Color tertiaryBackground = const Color(0xFF334155); // Azul gris @override - Color transparentBackground = const Color(0XFF4D4D4D).withOpacity(.2); + Color transparentBackground = + const Color(0xFF1E293B).withOpacity(.3); // Azul oscuro transparente @override - Color primaryText = Color(0xFFD7D7D7); + Color primaryText = const Color(0xFFFFFFFF); // Blanco para contraste @override - Color secondaryText = Color(0xFF5C63C8); + Color secondaryText = const Color(0xFFF1F5F9); // Gris muy claro @override - Color tertiaryText = const Color(0XFF747474); + Color tertiaryText = const Color(0xFF94A3B8); // Gris azulado claro @override - Color hintText = const Color(0XFF8A88A0); + Color hintText = const Color(0xFF64748B); // Gris medio @override - Color error = const Color(0XFFF44A49); + Color error = const Color(0xFFEF4444); // Rojo moderno @override - Color warning = const Color(0XFFF5AB1A); + Color warning = const Color(0xFFF59E0B); // Amarillo moderno @override - Color success = const Color(0XFF3AC170); + Color success = const Color(0xFF10B981); // Verde esmeralda del login @override - Color formBackground = const Color(0xFF663DD9).withOpacity(.2); + Color formBackground = + const Color(0xFF3B82F6).withOpacity(.1); // Azul transparente DarkModeTheme({Mode? mode}) { if (mode != null) {