base creada
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
690
lib/providers/videos_provider.dart
Normal file
690
lib/providers/videos_provider.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user