import 'dart:async'; import 'package:file_picker/file_picker.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/utils/extensions.dart'; import 'package:flux/features/attachments/data/attachments_repository.dart'; import 'package:flux/features/attachments/models/attachment_model.dart'; import 'package:get_it/get_it.dart'; import 'package:image_picker/image_picker.dart'; part 'attachments_events.dart'; part 'attachments_state.dart'; class AttachmentsBloc extends Bloc { final _repository = GetIt.I.get(); AttachmentsBloc({String? parentId, required AttachmentParentType parentType}) : super( AttachmentsState( status: AttachmentsStatus.initial, parentId: parentId, parentType: parentType, ), ) { on(_onParentEntitySaved); on(_onLoadAttachments); on(_onAddAttachments); on(_onUploadAttachments); on(_onDeleteAttachments); on(_onToggleAttachmentSelection); on(_onLinkAttachmentsToEntity); on(_onRenameAttachment); on(_onDeleteSpecificAttachment); on(_onSelectAllAttachments); on(_onClearAttachmentSelection); // Se il BLoC nasce già con un ID, carichiamo i file if (parentId != null) { add(LoadAttachmentsEvent(parentId: parentId)); } } FutureOr _onParentEntitySaved( ParentEntitySavedEvent event, Emitter emit, ) async { emit( state.copyWith( parentId: event.newParentId, status: AttachmentsStatus.uploading, ), ); if (state.localFiles.isNotEmpty) { try { final List> uploadTasks = state.localFiles.map((file) { final fakePlatformFile = PlatformFile( name: '${file.name}.${file.extension}', size: file.fileSize, bytes: file.localBytes, ); // Chiamiamo il metodo generico passando il parentId e il TYPE return _repository.uploadAndRegisterFile( parentId: event.newParentId, parentType: state.parentType, pickedFile: fakePlatformFile, ); }).toList(); await Future.wait(uploadTasks); } catch (e) { emit( state.copyWith( status: AttachmentsStatus.failure, error: "Errore upload post-salvataggio: $e", ), ); return; } } emit(state.copyWith(localFiles: [], status: AttachmentsStatus.success)); add(LoadAttachmentsEvent(parentId: event.newParentId)); } FutureOr _onLoadAttachments( LoadAttachmentsEvent event, Emitter emit, ) async { final currentId = event.parentId ?? state.parentId; if (currentId != null) { emit(state.copyWith(status: AttachmentsStatus.loading)); await emit.forEach( _repository.getFilesStream( currentId, state.parentType, ), // Passiamo il tipo! onData: (List data) => state.copyWith( status: AttachmentsStatus.success, remoteFiles: data, ), onError: (error, stackTrace) => state.copyWith( status: AttachmentsStatus.failure, error: error.toString(), ), ); } } void _onAddAttachments( AddAttachmentsEvent event, Emitter emit, ) async { final currentId = state.parentId; // BIVIO 1: PRATICA NUOVA (Salvataggio locale) if (currentId == null) { final companyId = GetIt.I.get().state.company!.id!; final newLocalFiles = event.files.map((file) { // Assegniamo i campi dinamicamente in base al parentType! return AttachmentModel( id: null, companyId: companyId, operationId: state.parentType == AttachmentParentType.operation ? '' : null, ticketId: state.parentType == AttachmentParentType.ticket ? '' : null, customerId: state.parentType == AttachmentParentType.customer ? '' : null, name: file.name.fileNameWithoutExtension(), extension: file.name.fileExtension(), storagePath: '', fileSize: file.size, localBytes: file.bytes, ); }).toList(); emit( state.copyWith( localFiles: [...state.localFiles, ...newLocalFiles], status: AttachmentsStatus.success, ), ); return; } // BIVIO 2: PRATICA ESISTENTE (Upload immediato) emit(state.copyWith(status: AttachmentsStatus.uploading)); try { final List> uploadTasks = event.files.map((file) { return _repository.uploadAndRegisterFile( parentId: currentId, parentType: state.parentType, pickedFile: file, ); }).toList(); await Future.wait(uploadTasks); emit(state.copyWith(status: AttachmentsStatus.success)); } catch (e) { emit( state.copyWith(status: AttachmentsStatus.failure, error: e.toString()), ); } } FutureOr _onUploadAttachments( UploadAttachmentsEvent event, Emitter emit, ) async { if ((event.pickedFiles == null || event.pickedFiles!.isEmpty) && (event.photos == null || event.photos!.isEmpty)) { return; } if (state.parentId == null) return; emit(state.copyWith(status: AttachmentsStatus.uploading)); try { final List> uploadTasks = []; // 1. Gestione Documenti normali (PlatformFile) if (event.pickedFiles != null) { for (var file in event.pickedFiles!) { uploadTasks.add( _repository.uploadAndRegisterFile( parentId: state.parentId!, parentType: state.parentType, pickedFile: file, ), ); } } // 2. Gestione Foto Fotocamera (XFile) if (event.photos != null) { for (var photo in event.photos!) { // Leggiamo i byte asincronamente final bytes = await photo.readAsBytes(); final fileSize = await photo.length(); // Lo travestiamo da PlatformFile per passarlo al Repository! final fakePlatformFile = PlatformFile( name: photo.name, size: fileSize, bytes: bytes, path: photo.path, ); uploadTasks.add( _repository.uploadAndRegisterFile( parentId: state.parentId!, parentType: state.parentType, pickedFile: fakePlatformFile, ), ); } } // Esecuzione parallela di tutti i documenti e foto await Future.wait(uploadTasks); emit(state.copyWith(status: AttachmentsStatus.success)); } catch (e) { emit( state.copyWith(status: AttachmentsStatus.failure, error: e.toString()), ); } } FutureOr _onDeleteAttachments( DeleteAttachmentsEvent event, Emitter emit, ) async { emit(state.copyWith(status: AttachmentsStatus.loading)); try { await _repository.deleteFiles( files: state.selectedFiles, currentContextType: state.parentType, ); emit( state.copyWith(status: AttachmentsStatus.success, selectedFiles: []), ); } catch (e) { emit( state.copyWith(status: AttachmentsStatus.failure, error: e.toString()), ); } } FutureOr _onToggleAttachmentSelection( ToggleAttachmentSelectionEvent event, Emitter emit, ) { final selectedFiles = List.from(state.selectedFiles); if (selectedFiles.contains(event.file)) { selectedFiles.remove(event.file); } else { selectedFiles.add(event.file); } emit(state.copyWith(selectedFiles: selectedFiles)); } void _onSelectAllAttachments( SelectAllAttachmentsEvent event, Emitter emit, ) { // Prendiamo TUTTI i file (locali e remoti) e li buttiamo nei selezionati emit(state.copyWith(selectedFiles: state.allFiles)); } void _onClearAttachmentSelection( ClearAttachmentSelectionEvent event, Emitter emit, ) { // Svuotiamo brutalmente la lista emit(state.copyWith(selectedFiles: [])); } FutureOr _onLinkAttachmentsToEntity( LinkAttachmentsToEntityEvent event, Emitter emit, ) async { if (state.selectedFiles.isEmpty) return; // BIVIO 1: PRATICA/TICKET NON ANCORA SALVATA (Modalità Locale) if (state.parentId == null) { final updatedLocalFiles = state.localFiles.map((file) { if (state.selectedFiles.contains(file)) { // Assegniamo dinamicamente l'ID in base all'entità scelta switch (event.targetType) { case AttachmentParentType.customer: return file.copyWith(customerId: event.targetId); case AttachmentParentType.ticket: return file.copyWith(ticketId: event.targetId); case AttachmentParentType.operation: return file.copyWith(operationId: event.targetId); } } return file; }).toList(); emit( state.copyWith( localFiles: updatedLocalFiles, selectedFiles: [], // Svuotiamo la selezione status: AttachmentsStatus.success, ), ); return; } // BIVIO 2: PRATICA/TICKET ESISTENTE (Modalità Remota su DB) emit(state.copyWith(status: AttachmentsStatus.loading)); try { final List> linkTasks = []; for (var file in state.selectedFiles) { if (file.id != null) { linkTasks.add( _repository.linkFileToEntity( fileId: file.id!, targetType: event.targetType, targetId: event.targetId, ), ); } } await Future.wait(linkTasks); // Lo stream aggiornerà automaticamente la UI emit( state.copyWith(status: AttachmentsStatus.success, selectedFiles: []), ); } catch (e) { emit( state.copyWith( status: AttachmentsStatus.failure, error: "Errore durante il collegamento: $e", ), ); } } FutureOr _onRenameAttachment( RenameAttachmentEvent event, Emitter emit, ) async { // BIVIO 1: File Locale (Bozza) if (event.file.localBytes != null) { final updatedLocalFiles = state.localFiles.map((f) { if (f == event.file) { return f.copyWith(name: event.newName); } return f; }).toList(); emit(state.copyWith(localFiles: updatedLocalFiles)); return; } // BIVIO 2: File Remoto (Salvato su DB) emit(state.copyWith(status: AttachmentsStatus.loading)); try { await _repository.renameAttachment(event.file.id!, event.newName); emit(state.copyWith(status: AttachmentsStatus.success)); } catch (e) { emit( state.copyWith( status: AttachmentsStatus.failure, error: "Errore rinomina: $e", ), ); } } FutureOr _onDeleteSpecificAttachment( DeleteSpecificAttachmentEvent event, Emitter emit, ) { if (event.file.localBytes != null) { final updatedLocalFiles = state.localFiles .where((f) => f != event.file) .toList(); emit(state.copyWith(localFiles: updatedLocalFiles)); } } }