base creada

This commit is contained in:
Abraham
2026-01-10 21:12:17 -08:00
parent 8bfc7d60c3
commit 9adadbd354
62 changed files with 5392 additions and 22447 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,418 +0,0 @@
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<Empresa> empresas = [];
List<Negocio> negocios = [];
List<PlutoRow> empresasRows = [];
List<PlutoRow> negociosRows = [];
// Variables para formularios
String? logoFileName;
String? imagenFileName;
Uint8List? logoToUpload;
Uint8List? imagenToUpload;
// Variables de selección
String? empresaSeleccionadaId;
Empresa? empresaSeleccionada;
// Variable para controlar si el provider está activo
bool _isDisposed = false;
EmpresasNegociosProvider() {
getEmpresas();
}
@override
void dispose() {
_isDisposed = true;
busquedaEmpresaController.dispose();
busquedaNegocioController.dispose();
super.dispose();
}
// Método seguro para notificar listeners
void _safeNotifyListeners() {
if (!_isDisposed) {
notifyListeners();
}
}
// Métodos para empresas
Future<void> 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<dynamic>)
.map((empresa) => Empresa.fromMap(empresa))
.toList();
_buildEmpresasRows();
_safeNotifyListeners();
} 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<void> 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<dynamic>)
.map((negocio) => Negocio.fromMap(negocio))
.toList();
_buildNegociosRows();
_safeNotifyListeners();
} 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),
'direccion_completa': PlutoCell(
value: negocio.direccion), // Nuevo campo para la segunda columna
'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}"
: '',
),
'acceder_infraestructura': PlutoCell(value: negocio.id),
'editar': PlutoCell(value: negocio.id),
'eliminar': PlutoCell(value: negocio.id),
'ver_componentes': PlutoCell(value: negocio.id),
}));
}
}
// Métodos para subir archivos
Future<void> 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;
// Notificar inmediatamente después de seleccionar
_safeNotifyListeners();
}
}
Future<void> 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;
// Notificar inmediatamente después de seleccionar
_safeNotifyListeners();
}
}
Future<String?> 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<String?> 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<bool> 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<bool> 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<bool> 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);
// Solo actualizar si el provider sigue activo
if (!_isDisposed) {
await getEmpresas();
}
return true;
} catch (e) {
print('Error en eliminarEmpresa: ${e.toString()}');
return false;
}
}
Future<bool> eliminarNegocio(String negocioId) async {
try {
await supabaseLU.from('negocio').delete().eq('id', negocioId);
// Solo actualizar si el provider sigue activo y hay una empresa seleccionada
if (!_isDisposed && 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);
_safeNotifyListeners();
}
void resetFormData() {
logoFileName = null;
imagenFileName = null;
logoToUpload = null;
imagenToUpload = null;
_safeNotifyListeners();
}
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,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
),
child: Image.asset(
'assets/images/placeholder_no_image.jpg',
height: height,
width: width,
fit: BoxFit.cover,
),
);
} else if (image is Uint8List) {
return Image.memory(
image,
height: height,
width: width,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Image.asset(
'assets/images/placeholder_no_image.jpg',
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 Image.asset(
'assets/images/placeholder_no_image.jpg',
height: height,
width: width,
fit: BoxFit.cover,
);
},
);
}
return Image.asset(
'assets/images/placeholder_no_image.jpg',
height: height,
width: width,
fit: BoxFit.cover,
);
}
}

View File

@@ -1,128 +0,0 @@
import 'package:flutter/material.dart';
import 'package:nethive_neo/models/nethive/negocio_model.dart';
import 'package:nethive_neo/models/nethive/empresa_model.dart';
import 'package:nethive_neo/helpers/globals.dart';
class NavigationProvider extends ChangeNotifier {
// Estados principales
String? _negocioSeleccionadoId;
Negocio? _negocioSeleccionado;
Empresa? _empresaSeleccionada;
int _selectedMenuIndex = 0;
// Getters
String? get negocioSeleccionadoId => _negocioSeleccionadoId;
Negocio? get negocioSeleccionado => _negocioSeleccionado;
Empresa? get empresaSeleccionada => _empresaSeleccionada;
int get selectedMenuIndex => _selectedMenuIndex;
// Lista de opciones del sidemenu
final List<NavigationMenuItem> menuItems = [
NavigationMenuItem(
title: 'Dashboard',
icon: Icons.dashboard,
route: '/dashboard',
index: 0,
),
NavigationMenuItem(
title: 'Inventario',
icon: Icons.inventory_2,
route: '/inventario',
index: 1,
),
NavigationMenuItem(
title: 'Topología',
icon: Icons.account_tree,
route: '/topologia',
index: 2,
),
NavigationMenuItem(
title: 'Alertas',
icon: Icons.warning,
route: '/alertas',
index: 3,
),
NavigationMenuItem(
title: 'Configuración',
icon: Icons.settings,
route: '/configuracion',
index: 4,
),
NavigationMenuItem(
title: 'Empresas',
icon: Icons.business,
route: '/empresas',
index: 5,
isSpecial: true, // Para diferenciarlo como opción de regreso
),
];
// Métodos para establecer el negocio seleccionado
Future<void> setNegocioSeleccionado(String negocioId) async {
try {
_negocioSeleccionadoId = negocioId;
// Obtener datos completos del negocio
final negocioResponse = await supabaseLU.from('negocio').select('''
*,
empresa!inner(*)
''').eq('id', negocioId).single();
_negocioSeleccionado = Negocio.fromMap(negocioResponse);
_empresaSeleccionada = Empresa.fromMap(negocioResponse['empresa']);
// Reset menu selection when changing business
_selectedMenuIndex = 0;
notifyListeners();
} catch (e) {
print('Error al establecer negocio seleccionado: $e');
}
}
// Método para cambiar la selección del menú
void setSelectedMenuIndex(int index) {
_selectedMenuIndex = index;
notifyListeners();
}
// Método para limpiar la selección (al regresar a empresas)
void clearSelection() {
_negocioSeleccionadoId = null;
_negocioSeleccionado = null;
_empresaSeleccionada = null;
_selectedMenuIndex = 0;
notifyListeners();
}
// Método para obtener el item del menú por índice
NavigationMenuItem getMenuItemByIndex(int index) {
return menuItems.firstWhere((item) => item.index == index);
}
// Método para obtener el item del menú por ruta
NavigationMenuItem? getMenuItemByRoute(String route) {
try {
return menuItems.firstWhere((item) => item.route == route);
} catch (e) {
return null;
}
}
}
// Modelo para los items del menú
class NavigationMenuItem {
final String title;
final IconData icon;
final String route;
final int index;
final bool isSpecial;
NavigationMenuItem({
required this.title,
required this.icon,
required this.route,
required this.index,
this.isSpecial = false,
});
}

View File

@@ -1,6 +1,3 @@
export 'package:nethive_neo/providers/visual_state_provider.dart';
export 'package:nethive_neo/providers/users_provider.dart';
export 'package:nethive_neo/providers/user_provider.dart';
export 'package:nethive_neo/providers/nethive/empresas_negocios_provider.dart';
export 'package:nethive_neo/providers/nethive/componentes_provider.dart';
export 'package:nethive_neo/providers/nethive/navigation_provider.dart';

View File

@@ -0,0 +1,690 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:pluto_grid/pluto_grid.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:path/path.dart' as p;
import 'package:nethive_neo/helpers/globals.dart';
import 'package:nethive_neo/models/media/media_models.dart';
class VideosProvider extends ChangeNotifier {
// ========== ORGANIZATION CONSTANT ==========
static const int organizationId = 17;
// ========== STATE MANAGEMENT ==========
PlutoGridStateManager? stateManager;
List<PlutoRow> videosRows = [];
// ========== DATA LISTS ==========
List<MediaFileModel> mediaFiles = [];
List<MediaCategoryModel> categories = [];
List<MediaWithPosterModel> mediaWithPosters = [];
// ========== CONTROLLERS ==========
final busquedaVideoController = TextEditingController();
final tituloController = TextEditingController();
final descripcionController = TextEditingController();
// ========== VIDEO/IMAGE UPLOAD STATE ==========
String? videoName;
String? videoUrl;
String? videoStoragePath;
String videoFileExtension = '';
Uint8List? webVideoBytes;
String? posterName;
String? posterUrl;
String? posterStoragePath;
String posterFileExtension = '';
Uint8List? webPosterBytes;
// ========== LOADING STATE ==========
bool isLoading = false;
String? errorMessage;
// ========== CONSTRUCTOR ==========
VideosProvider() {
loadMediaFiles();
loadCategories();
}
// ========== LOAD METHODS ==========
/// Load all media files with organization filter
Future<void> loadMediaFiles() async {
try {
isLoading = true;
errorMessage = null;
notifyListeners();
final response = await supabaseML
.from('media_files')
.select()
.eq('organization_fk', organizationId)
.order('created_at_timestamp', ascending: false);
mediaFiles = (response as List<dynamic>)
.map((item) => MediaFileModel.fromMap(item))
.toList();
await _buildPlutoRows();
isLoading = false;
notifyListeners();
} catch (e) {
errorMessage = 'Error cargando videos: $e';
isLoading = false;
notifyListeners();
print('Error en loadMediaFiles: $e');
}
}
/// Load media files with posters using view
Future<void> loadMediaWithPosters() async {
try {
isLoading = true;
notifyListeners();
final response = await supabaseML
.from('vw_media_files_with_posters')
.select()
.eq('organization_fk', organizationId)
.order('media_created_at', ascending: false);
mediaWithPosters = (response as List<dynamic>)
.map((item) => MediaWithPosterModel.fromMap(item))
.toList();
isLoading = false;
notifyListeners();
} catch (e) {
errorMessage = 'Error cargando videos con posters: $e';
isLoading = false;
notifyListeners();
print('Error en loadMediaWithPosters: $e');
}
}
/// Load all categories
Future<void> loadCategories() async {
try {
final response = await supabaseML
.from('media_categories')
.select()
.order('category_name');
categories = (response as List<dynamic>)
.map((item) => MediaCategoryModel.fromMap(item))
.toList();
notifyListeners();
} catch (e) {
print('Error en loadCategories: $e');
}
}
/// Build PlutoGrid rows from media files
Future<void> _buildPlutoRows() async {
videosRows.clear();
for (var media in mediaFiles) {
videosRows.add(
PlutoRow(
cells: {
'id': PlutoCell(value: media.mediaFileId),
'thumbnail':
PlutoCell(value: media.fileUrl), // Para mostrar thumbnail
'title': PlutoCell(value: media.title ?? media.fileName),
'description': PlutoCell(value: media.fileDescription ?? ''),
'category':
PlutoCell(value: _getCategoryName(media.mediaCategoryFk)),
'reproducciones': PlutoCell(value: media.reproducciones),
'duration': PlutoCell(value: media.seconds ?? 0),
'size': PlutoCell(value: _formatFileSize(media.fileSizeBytes)),
'created_at': PlutoCell(value: media.createdAt),
'actions': PlutoCell(value: media.mediaFileId),
},
),
);
}
}
/// Get category name by ID
String _getCategoryName(int? categoryId) {
if (categoryId == null) return 'Sin categoría';
try {
return categories
.firstWhere((cat) => cat.mediaCategoriesId == categoryId)
.categoryName;
} catch (e) {
return 'Sin categoría';
}
}
/// Format file size to human readable
String _formatFileSize(int? bytes) {
if (bytes == null) return '-';
if (bytes < 1024) return '$bytes B';
if (bytes < 1048576) return '${(bytes / 1024).toStringAsFixed(1)} KB';
if (bytes < 1073741824) return '${(bytes / 1048576).toStringAsFixed(1)} MB';
return '${(bytes / 1073741824).toStringAsFixed(1)} GB';
}
// ========== VIDEO UPLOAD ==========
/// Select video file from device
Future<bool> selectVideo() async {
try {
final ImagePicker picker = ImagePicker();
final XFile? pickedVideo = await picker.pickVideo(
source: ImageSource.gallery,
);
if (pickedVideo == null) return false;
videoName = pickedVideo.name;
videoFileExtension = p.extension(pickedVideo.name);
webVideoBytes = await pickedVideo.readAsBytes();
// Remove extension from name for title
final nameWithoutExt = videoName!.replaceAll(videoFileExtension, '');
tituloController.text = nameWithoutExt;
notifyListeners();
return true;
} catch (e) {
errorMessage = 'Error seleccionando video: $e';
notifyListeners();
return false;
}
}
/// Select poster/thumbnail image
Future<bool> selectPoster() async {
try {
final ImagePicker picker = ImagePicker();
final XFile? pickedImage = await picker.pickImage(
source: ImageSource.gallery,
);
if (pickedImage == null) return false;
posterName = pickedImage.name;
posterFileExtension = p.extension(pickedImage.name);
webPosterBytes = await pickedImage.readAsBytes();
notifyListeners();
return true;
} catch (e) {
errorMessage = 'Error seleccionando poster: $e';
notifyListeners();
return false;
}
}
/// Upload video to Supabase Storage and create record
Future<bool> uploadVideo({
required String title,
String? description,
int? categoryId,
int? durationSeconds,
}) async {
if (webVideoBytes == null || videoName == null) {
errorMessage = 'No hay video seleccionado';
notifyListeners();
return false;
}
try {
isLoading = true;
notifyListeners();
// 1. Upload video to storage
final timestamp = DateTime.now().millisecondsSinceEpoch;
final fileName = '${timestamp}_$videoName';
videoStoragePath = 'videos/$fileName';
await supabaseML.storage.from('energymedia').uploadBinary(
videoStoragePath!,
webVideoBytes!,
fileOptions: const FileOptions(
cacheControl: '3600',
upsert: false,
),
);
// 2. Get public URL
videoUrl = supabaseML.storage
.from('energymedia')
.getPublicUrl(videoStoragePath!);
// 3. Upload poster if exists
int? posterFileId;
if (webPosterBytes != null && posterName != null) {
posterFileId = await _uploadPoster();
}
// 4. Create media_files record
final metadataJson = {
'uploaded_at': DateTime.now().toIso8601String(),
'reproducciones': 0,
'original_file_name': videoName,
'duration_seconds': durationSeconds,
};
final response = await supabaseML.from('media_files').insert({
'file_name': fileName,
'title': title,
'file_description': description,
'file_type': 'video',
'mime_type': _getMimeType(videoFileExtension),
'file_extension': videoFileExtension,
'file_size_bytes': webVideoBytes!.length,
'file_url': videoUrl,
'storage_path': videoStoragePath,
'organization_fk': organizationId,
'media_category_fk': categoryId,
'metadata_json': metadataJson,
'seconds': durationSeconds,
'is_public_file': true,
'uploaded_by_user_id': currentUser?.id,
}).select();
// 5. Create poster relationship if exists
if (posterFileId != null && response.isNotEmpty) {
final mediaFileId = response[0]['media_file_id'];
await supabaseML.from('media_posters').insert({
'media_file_id': mediaFileId,
'poster_file_id': posterFileId,
});
}
// Clean up
_clearUploadState();
// Reload data
await loadMediaFiles();
isLoading = false;
notifyListeners();
return true;
} catch (e) {
errorMessage = 'Error subiendo video: $e';
isLoading = false;
notifyListeners();
print('Error en uploadVideo: $e');
return false;
}
}
/// Upload poster image (internal helper)
Future<int?> _uploadPoster() async {
if (webPosterBytes == null || posterName == null) return null;
try {
final timestamp = DateTime.now().millisecondsSinceEpoch;
final fileName = '${timestamp}_$posterName';
posterStoragePath = 'imagenes/$fileName';
await supabaseML.storage.from('energymedia').uploadBinary(
posterStoragePath!,
webPosterBytes!,
fileOptions: const FileOptions(
cacheControl: '3600',
upsert: false,
),
);
posterUrl = supabaseML.storage
.from('energymedia')
.getPublicUrl(posterStoragePath!);
// Create media_files record for poster
final response = await supabaseML.from('media_files').insert({
'file_name': fileName,
'title': 'Poster',
'file_type': 'image',
'mime_type': _getMimeType(posterFileExtension),
'file_extension': posterFileExtension,
'file_size_bytes': webPosterBytes!.length,
'file_url': posterUrl,
'storage_path': posterStoragePath,
'organization_fk': organizationId,
'is_public_file': true,
'uploaded_by_user_id': currentUser?.id,
}).select();
return response[0]['media_file_id'] as int;
} catch (e) {
print('Error en _uploadPoster: $e');
return null;
}
}
/// Get MIME type from file extension
String _getMimeType(String extension) {
final ext = extension.toLowerCase().replaceAll('.', '');
switch (ext) {
case 'mp4':
return 'video/mp4';
case 'webm':
return 'video/webm';
case 'mov':
return 'video/quicktime';
case 'avi':
return 'video/x-msvideo';
case 'jpg':
case 'jpeg':
return 'image/jpeg';
case 'png':
return 'image/png';
case 'gif':
return 'image/gif';
default:
return 'application/octet-stream';
}
}
/// Clear upload state
void _clearUploadState() {
videoName = null;
videoUrl = null;
videoStoragePath = null;
videoFileExtension = '';
webVideoBytes = null;
posterName = null;
posterUrl = null;
posterStoragePath = null;
posterFileExtension = '';
webPosterBytes = null;
tituloController.clear();
descripcionController.clear();
}
// ========== UPDATE METHODS ==========
/// Update video title
Future<bool> updateVideoTitle(int mediaFileId, String title) async {
try {
await supabaseML
.from('media_files')
.update({'title': title})
.eq('media_file_id', mediaFileId)
.eq('organization_fk', organizationId);
await loadMediaFiles();
return true;
} catch (e) {
errorMessage = 'Error actualizando título: $e';
notifyListeners();
print('Error en updateVideoTitle: $e');
return false;
}
}
/// Update video description
Future<bool> updateVideoDescription(
int mediaFileId, String description) async {
try {
await supabaseML
.from('media_files')
.update({'file_description': description})
.eq('media_file_id', mediaFileId)
.eq('organization_fk', organizationId);
await loadMediaFiles();
return true;
} catch (e) {
errorMessage = 'Error actualizando descripción: $e';
notifyListeners();
print('Error en updateVideoDescription: $e');
return false;
}
}
/// Update video category
Future<bool> updateVideoCategory(int mediaFileId, int? categoryId) async {
try {
await supabaseML
.from('media_files')
.update({'media_category_fk': categoryId})
.eq('media_file_id', mediaFileId)
.eq('organization_fk', organizationId);
await loadMediaFiles();
return true;
} catch (e) {
errorMessage = 'Error actualizando categoría: $e';
notifyListeners();
print('Error en updateVideoCategory: $e');
return false;
}
}
/// Update video metadata
Future<bool> updateVideoMetadata(
int mediaFileId,
Map<String, dynamic> metadata,
) async {
try {
await supabaseML
.from('media_files')
.update({'metadata_json': metadata})
.eq('media_file_id', mediaFileId)
.eq('organization_fk', organizationId);
await loadMediaFiles();
return true;
} catch (e) {
errorMessage = 'Error actualizando metadata: $e';
notifyListeners();
print('Error en updateVideoMetadata: $e');
return false;
}
}
// ========== DELETE METHODS ==========
/// Delete video and its storage files
Future<bool> deleteVideo(int mediaFileId) async {
try {
isLoading = true;
notifyListeners();
// Get video info
final response = await supabaseML
.from('media_files')
.select()
.eq('media_file_id', mediaFileId)
.eq('organization_fk', organizationId)
.single();
final storagePath = response['storage_path'] as String?;
// Delete from storage if path exists
if (storagePath != null) {
await supabaseML.storage.from('energymedia').remove([storagePath]);
}
// Delete associated posters
final posters = await supabaseML
.from('media_posters')
.select('poster_file_id')
.eq('media_file_id', mediaFileId);
for (var poster in posters) {
await _deletePosterFile(poster['poster_file_id']);
}
// Delete database record (cascade will delete posters relationship)
await supabaseML
.from('media_files')
.delete()
.eq('media_file_id', mediaFileId)
.eq('organization_fk', organizationId);
await loadMediaFiles();
isLoading = false;
notifyListeners();
return true;
} catch (e) {
errorMessage = 'Error eliminando video: $e';
isLoading = false;
notifyListeners();
print('Error en deleteVideo: $e');
return false;
}
}
/// Delete poster file (internal helper)
Future<void> _deletePosterFile(int posterFileId) async {
try {
final response = await supabaseML
.from('media_files')
.select('storage_path')
.eq('media_file_id', posterFileId)
.single();
final storagePath = response['storage_path'] as String?;
if (storagePath != null) {
await supabaseML.storage.from('energymedia').remove([storagePath]);
}
await supabaseML
.from('media_files')
.delete()
.eq('media_file_id', posterFileId);
} catch (e) {
print('Error en _deletePosterFile: $e');
}
}
// ========== ANALYTICS METHODS ==========
/// Increment view count
Future<bool> incrementReproduccion(int mediaFileId) async {
try {
// Get current metadata
final response = await supabaseML
.from('media_files')
.select('metadata_json')
.eq('media_file_id', mediaFileId)
.eq('organization_fk', organizationId)
.single();
final metadata = response['metadata_json'] as Map<String, dynamic>? ?? {};
final currentCount = metadata['reproducciones'] ?? 0;
metadata['reproducciones'] = currentCount + 1;
metadata['last_viewed_at'] = DateTime.now().toIso8601String();
await updateVideoMetadata(mediaFileId, metadata);
return true;
} catch (e) {
print('Error en incrementReproduccion: $e');
return false;
}
}
/// Get dashboard statistics
Future<Map<String, dynamic>> getDashboardStats() async {
try {
// Total videos
final totalVideos = mediaFiles.length;
// Total reproducciones
int totalReproducciones = 0;
for (var media in mediaFiles) {
totalReproducciones += media.reproducciones;
}
// Most viewed video
MediaFileModel? mostViewed;
if (mediaFiles.isNotEmpty) {
mostViewed = mediaFiles.reduce((curr, next) =>
curr.reproducciones > next.reproducciones ? curr : next);
}
// Videos by category
Map<String, int> videosByCategory = {};
for (var media in mediaFiles) {
final categoryName = _getCategoryName(media.mediaCategoryFk);
videosByCategory[categoryName] =
(videosByCategory[categoryName] ?? 0) + 1;
}
// Most viewed category
String? mostViewedCategory;
if (videosByCategory.isNotEmpty) {
mostViewedCategory = videosByCategory.entries
.reduce((a, b) => a.value > b.value ? a : b)
.key;
}
return {
'total_videos': totalVideos,
'total_reproducciones': totalReproducciones,
'most_viewed_video': mostViewed?.toMap(),
'videos_by_category': videosByCategory,
'most_viewed_category': mostViewedCategory,
'total_categories': categories.length,
};
} catch (e) {
print('Error en getDashboardStats: $e');
return {};
}
}
// ========== SEARCH & FILTER ==========
/// Search videos by title or description
void searchVideos(String query) {
if (query.isEmpty) {
_buildPlutoRows();
notifyListeners();
return;
}
videosRows.clear();
final filteredMedia = mediaFiles.where((media) {
final title = (media.title ?? media.fileName).toLowerCase();
final description = (media.fileDescription ?? '').toLowerCase();
final searchQuery = query.toLowerCase();
return title.contains(searchQuery) || description.contains(searchQuery);
}).toList();
for (var media in filteredMedia) {
videosRows.add(
PlutoRow(
cells: {
'id': PlutoCell(value: media.mediaFileId),
'thumbnail': PlutoCell(value: media.fileUrl),
'title': PlutoCell(value: media.title ?? media.fileName),
'description': PlutoCell(value: media.fileDescription ?? ''),
'category':
PlutoCell(value: _getCategoryName(media.mediaCategoryFk)),
'reproducciones': PlutoCell(value: media.reproducciones),
'duration': PlutoCell(value: media.seconds ?? 0),
'size': PlutoCell(value: _formatFileSize(media.fileSizeBytes)),
'created_at': PlutoCell(value: media.createdAt),
'actions': PlutoCell(value: media.mediaFileId),
},
),
);
}
notifyListeners();
}
// ========== CLEANUP ==========
@override
void dispose() {
busquedaVideoController.dispose();
tituloController.dispose();
descripcionController.dispose();
super.dispose();
}
}

View File

@@ -167,6 +167,12 @@ class VisualStateProvider extends ChangeNotifier {
isTaped[index] = true;
}
void changeThemeMode(ThemeMode mode, BuildContext context) {
AppTheme.saveThemeMode(mode);
setDarkModeSetting(context, mode);
notifyListeners();
}
@override
void dispose() {
primaryColorLightController.dispose();