Files
flux/lib/features/attachments/blocs/attachments_bloc.dart
2026-05-09 09:50:20 +02:00

396 lines
12 KiB
Dart

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<AttachmentsEvent, AttachmentsState> {
final _repository = GetIt.I.get<AttachmentsRepository>();
final String? companyId = GetIt.I.get<SessionCubit>().state.company?.id;
AttachmentsBloc({String? parentId, required AttachmentParentType parentType})
: super(
AttachmentsState(
status: AttachmentsStatus.initial,
parentId: parentId,
parentType: parentType,
),
) {
on<ParentEntitySavedEvent>(_onParentEntitySaved);
on<LoadAttachmentsEvent>(_onLoadAttachments);
on<AddAttachmentsEvent>(_onAddAttachments);
on<UploadAttachmentsEvent>(_onUploadAttachments);
on<DeleteAttachmentsEvent>(_onDeleteAttachments);
on<ToggleAttachmentSelectionEvent>(_onToggleAttachmentSelection);
on<LinkAttachmentsToEntityEvent>(_onLinkAttachmentsToEntity);
on<RenameAttachmentEvent>(_onRenameAttachment);
on<DeleteSpecificAttachmentEvent>(_onDeleteSpecificAttachment);
on<SelectAllAttachmentsEvent>(_onSelectAllAttachments);
on<ClearAttachmentSelectionEvent>(_onClearAttachmentSelection);
// Se il BLoC nasce già con un ID, carichiamo i file
if (parentId != null && companyId != null) {
add(LoadAttachmentsEvent(parentId: parentId));
}
}
FutureOr<void> _onParentEntitySaved(
ParentEntitySavedEvent event,
Emitter<AttachmentsState> emit,
) async {
emit(
state.copyWith(
parentId: event.newParentId,
status: AttachmentsStatus.uploading,
),
);
if (state.localFiles.isNotEmpty) {
try {
final List<Future<void>> 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,
companyId: companyId!,
);
}).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<void> _onLoadAttachments(
LoadAttachmentsEvent event,
Emitter<AttachmentsState> 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<AttachmentModel> data) => state.copyWith(
status: AttachmentsStatus.success,
remoteFiles: data,
),
onError: (error, stackTrace) => state.copyWith(
status: AttachmentsStatus.failure,
error: error.toString(),
),
);
}
}
void _onAddAttachments(
AddAttachmentsEvent event,
Emitter<AttachmentsState> emit,
) async {
final currentId = state.parentId;
// BIVIO 1: PRATICA NUOVA (Salvataggio locale)
if (currentId == null) {
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<Future<void>> uploadTasks = event.files.map((file) {
return _repository.uploadAndRegisterFile(
parentId: currentId,
parentType: state.parentType,
pickedFile: file,
companyId: companyId!,
);
}).toList();
await Future.wait(uploadTasks);
emit(state.copyWith(status: AttachmentsStatus.success));
} catch (e) {
emit(
state.copyWith(status: AttachmentsStatus.failure, error: e.toString()),
);
}
}
FutureOr<void> _onUploadAttachments(
UploadAttachmentsEvent event,
Emitter<AttachmentsState> 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<Future<void>> 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,
companyId: event.companyId,
),
);
}
}
// 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,
companyId: companyId!,
),
);
}
}
// 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<void> _onDeleteAttachments(
DeleteAttachmentsEvent event,
Emitter<AttachmentsState> 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<void> _onToggleAttachmentSelection(
ToggleAttachmentSelectionEvent event,
Emitter<AttachmentsState> emit,
) {
final selectedFiles = List<AttachmentModel>.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<AttachmentsState> emit,
) {
// Prendiamo TUTTI i file (locali e remoti) e li buttiamo nei selezionati
emit(state.copyWith(selectedFiles: state.allFiles));
}
void _onClearAttachmentSelection(
ClearAttachmentSelectionEvent event,
Emitter<AttachmentsState> emit,
) {
// Svuotiamo brutalmente la lista
emit(state.copyWith(selectedFiles: []));
}
FutureOr<void> _onLinkAttachmentsToEntity(
LinkAttachmentsToEntityEvent event,
Emitter<AttachmentsState> 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<Future<void>> 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<void> _onRenameAttachment(
RenameAttachmentEvent event,
Emitter<AttachmentsState> 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<void> _onDeleteSpecificAttachment(
DeleteSpecificAttachmentEvent event,
Emitter<AttachmentsState> emit,
) {
if (event.file.localBytes != null) {
final updatedLocalFiles = state.localFiles
.where((f) => f != event.file)
.toList();
emit(state.copyWith(localFiles: updatedLocalFiles));
}
}
}