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/models/attachment_model.dart'; import 'package:flux/features/operations/data/operations_repository.dart'; import 'package:get_it/get_it.dart'; import 'package:image_picker/image_picker.dart'; part 'operation_files_events.dart'; part 'operation_files_state.dart'; class OperationFilesBloc extends Bloc { final _repository = GetIt.I.get(); final String? operationId; OperationFilesBloc({this.operationId}) : super( OperationFilesState( status: OperationFilesStatus.initial, operationId: operationId, ), ) { on(_onOperationsaved); on(_onLoadOperationFiles); on(_onAddOperationFiles); on(_onUploadOperationFiles); on(_onDeleteOperationFiles); on(_onToggleOperationFileSelection); on(_onLinkFilesToCustomer); // Se il BLoC nasce con un ID, accendiamo subito lo stream! if (operationId != null) { add(LoadOperationFilesEvent(operationId: operationId)); } } FutureOr _onOperationsaved( OperationsavedEvent event, Emitter emit, ) async { // 1. Aggiorniamo l'ID e mettiamo in loading emit( state.copyWith( operationId: event.operationId, status: OperationFilesStatus.uploading, ), ); // 2. RECUPERO E UPLOAD DEI FILE "PARCHEGGIATI" (Pratica Nuova) if (state.localFiles.isNotEmpty) { try { final List> uploadTasks = []; for (var file in state.localFiles) { // Ricreiamo il PlatformFile dal nostro AttachmentModel // così il repository lo accetta senza fare storie! final fakePlatformFile = PlatformFile( name: '${file.name}.${file.extension}', size: file.fileSize, bytes: file.localBytes, ); uploadTasks.add( _repository.uploadAndRegisterOperationFile( operationId: event.operationId, // L'ID APPENA NATO! pickedFile: fakePlatformFile, ), ); } // Lanciamo tutti gli upload in parallelo await Future.wait(uploadTasks); } catch (e) { emit( state.copyWith( status: OperationFilesStatus.failure, error: "Errore upload post-salvataggio: $e", ), ); return; // Ci fermiamo qui se esplode qualcosa } } // 3. FINE DEI GIOCHI! Svuotiamo i locali, passiamo a success e accendiamo lo Stream emit(state.copyWith(localFiles: [], status: OperationFilesStatus.success)); add(LoadOperationFilesEvent(operationId: event.operationId)); } FutureOr _onLoadOperationFiles( LoadOperationFilesEvent event, Emitter emit, ) async { final currentId = event.operationId ?? state.operationId; if (currentId != null) { emit(state.copyWith(status: OperationFilesStatus.loading)); await emit.forEach( _repository.getOperationFilesStream(currentId), onData: (List data) => state.copyWith( status: OperationFilesStatus.success, remoteFiles: data, ), onError: (error, stackTrace) => state.copyWith( status: OperationFilesStatus.failure, error: error.toString(), ), ); } } void _onAddOperationFiles( AddOperationFilesEvent event, Emitter emit, ) async { final currentId = state.operationId; // BIVIO 1: PRATICA NUOVA (Nessun ID - salvataggio locale) if (currentId == null) { final companyId = GetIt.I.get().state.company!.id!; final newLocalFiles = event.files.map((file) { return AttachmentModel( id: null, companyId: companyId, operationId: '', // Sarà riempito al salvataggio name: file.name.fileNameWithoutExtension(), extension: file.name.fileExtension(), storagePath: '', fileSize: file.size, localBytes: file.bytes, ); }).toList(); emit( state.copyWith( localFiles: [...state.localFiles, ...newLocalFiles], status: OperationFilesStatus.success, ), ); return; } // BIVIO 2: PRATICA ESISTENTE (Abbiamo l'ID - Upload immediato) emit(state.copyWith(status: OperationFilesStatus.uploading)); try { final List> uploadTasks = []; for (var file in event.files) { uploadTasks.add( _repository.uploadAndRegisterOperationFile( operationId: currentId, pickedFile: file, ), ); } await Future.wait(uploadTasks); emit(state.copyWith(status: OperationFilesStatus.success)); } catch (e) { emit( state.copyWith( status: OperationFilesStatus.failure, error: e.toString(), ), ); } } FutureOr _onUploadOperationFiles( UploadOperationFilesEvent event, Emitter emit, ) async { if ((event.pickedFiles == null || event.pickedFiles!.isEmpty) && (event.photos == null || event.photos!.isEmpty)) { return; } if (state.operationId == null) return; emit(state.copyWith(status: OperationFilesStatus.uploading)); try { final List> uploadTasks = []; // 1. Gestione Documenti normali (PlatformFile) if (event.pickedFiles != null) { for (var file in event.pickedFiles!) { uploadTasks.add( _repository.uploadAndRegisterOperationFile( operationId: state.operationId!, 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.uploadAndRegisterOperationFile( operationId: state.operationId!, pickedFile: fakePlatformFile, ), ); } } // Esecuzione parallela di tutti i documenti e foto await Future.wait(uploadTasks); emit(state.copyWith(status: OperationFilesStatus.success)); } catch (e) { emit( state.copyWith( status: OperationFilesStatus.failure, error: e.toString(), ), ); } } FutureOr _onDeleteOperationFiles( DeleteOperationFilesEvent event, Emitter emit, ) async { emit(state.copyWith(status: OperationFilesStatus.loading)); try { await _repository.deleteOperationFiles(state.selectedFiles); emit( state.copyWith(status: OperationFilesStatus.success, selectedFiles: []), ); } catch (e) { emit( state.copyWith( status: OperationFilesStatus.failure, error: e.toString(), ), ); } } FutureOr _onToggleOperationFileSelection( ToggleOperationFileSelectionEvent 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)); } FutureOr _onLinkFilesToCustomer( LinkFilesToCustomerEvent event, Emitter emit, ) async { if (state.selectedFiles.isEmpty) return; // BIVIO 1: PRATICA NUOVA (Modalità Locale) if (state.operationId == null) { // Mappiamo i file locali: se sono tra quelli selezionati, iniettiamo il customerId final updatedLocalFiles = state.localFiles.map((file) { if (state.selectedFiles.contains(file)) { return file.copyWith(customerId: event.customerId); } return file; }).toList(); emit( state.copyWith( localFiles: updatedLocalFiles, selectedFiles: [], // Svuotiamo la selezione dopo averli associati status: OperationFilesStatus.success, // o un toast di feedback ), ); return; } // BIVIO 2: PRATICA ESISTENTE (Modalità Remota su DB) emit(state.copyWith(status: OperationFilesStatus.loading)); try { final List> linkTasks = []; for (var file in state.selectedFiles) { linkTasks.add( _repository.copyFileToCustomer( file: file, customerId: event.customerId, ), ); } await Future.wait(linkTasks); // Svuotiamo la selezione. // NON serve aggiornare la lista a mano, perché il DB si aggiorna // e lo Stream di Supabase spingerà automaticamente in UI i file aggiornati! emit( state.copyWith(status: OperationFilesStatus.success, selectedFiles: []), ); } catch (e) { emit( state.copyWith( status: OperationFilesStatus.failure, error: "Errore associazione: $e", ), ); } } }