2026-05-07 16:28:01 +02:00
|
|
|
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>();
|
2026-05-09 09:50:20 +02:00
|
|
|
final String? companyId = GetIt.I.get<SessionCubit>().state.company?.id;
|
2026-05-07 16:28:01 +02:00
|
|
|
|
|
|
|
|
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
|
2026-05-09 09:50:20 +02:00
|
|
|
if (parentId != null && companyId != null) {
|
2026-05-07 16:28:01 +02:00
|
|
|
add(LoadAttachmentsEvent(parentId: parentId));
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-18 12:00:07 +02:00
|
|
|
|
2026-05-07 16:28:01 +02:00
|
|
|
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,
|
2026-05-09 09:50:20 +02:00
|
|
|
companyId: companyId!,
|
2026-05-18 12:00:07 +02:00
|
|
|
bucket: _getBucketForParentType,
|
2026-05-07 16:28:01 +02:00
|
|
|
);
|
|
|
|
|
}).toList();
|
|
|
|
|
|
|
|
|
|
await Future.wait(uploadTasks);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
|
|
|
|
status: AttachmentsStatus.failure,
|
|
|
|
|
error: "Errore upload post-salvataggio: $e",
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-09 11:43:54 +02:00
|
|
|
emit(state.copyWith(localFiles: [], status: AttachmentsStatus.ready));
|
2026-05-07 16:28:01 +02:00
|
|
|
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!
|
2026-05-09 11:43:54 +02:00
|
|
|
onData: (List<AttachmentModel> data) =>
|
|
|
|
|
state.copyWith(status: AttachmentsStatus.ready, remoteFiles: data),
|
2026-05-07 16:28:01 +02:00
|
|
|
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,
|
2026-05-09 09:50:20 +02:00
|
|
|
companyId: companyId!,
|
2026-05-07 16:28:01 +02:00
|
|
|
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],
|
2026-05-09 11:43:54 +02:00
|
|
|
status: AttachmentsStatus.ready,
|
2026-05-07 16:28:01 +02:00
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
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,
|
2026-05-09 09:50:20 +02:00
|
|
|
companyId: companyId!,
|
2026-05-18 12:00:07 +02:00
|
|
|
bucket: _getBucketForParentType,
|
2026-05-07 16:28:01 +02:00
|
|
|
);
|
|
|
|
|
}).toList();
|
|
|
|
|
|
|
|
|
|
await Future.wait(uploadTasks);
|
2026-05-09 11:43:54 +02:00
|
|
|
emit(state.copyWith(status: AttachmentsStatus.ready));
|
2026-05-07 16:28:01 +02:00
|
|
|
} 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,
|
2026-05-09 09:50:20 +02:00
|
|
|
companyId: event.companyId,
|
2026-05-18 12:00:07 +02:00
|
|
|
bucket: _getBucketForParentType,
|
2026-05-07 16:28:01 +02:00
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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,
|
2026-05-09 10:08:29 +02:00
|
|
|
companyId: event.companyId,
|
2026-05-18 12:00:07 +02:00
|
|
|
bucket: _getBucketForParentType,
|
2026-05-07 16:28:01 +02:00
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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,
|
2026-05-18 12:00:07 +02:00
|
|
|
bucket: _getBucketForParentType,
|
2026-05-07 16:28:01 +02:00
|
|
|
);
|
2026-05-09 11:43:54 +02:00
|
|
|
emit(state.copyWith(status: AttachmentsStatus.ready, selectedFiles: []));
|
2026-05-07 16:28:01 +02:00
|
|
|
} 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);
|
2026-05-18 12:00:07 +02:00
|
|
|
case AttachmentParentType.shippingDocument:
|
|
|
|
|
return file.copyWith(shippingDocumentId: event.targetId);
|
2026-05-07 16:28:01 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return file;
|
|
|
|
|
}).toList();
|
|
|
|
|
|
|
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
|
|
|
|
localFiles: updatedLocalFiles,
|
|
|
|
|
selectedFiles: [], // Svuotiamo la selezione
|
2026-05-09 11:43:54 +02:00
|
|
|
status: AttachmentsStatus.ready,
|
2026-05-07 16:28:01 +02:00
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
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
|
2026-05-09 11:43:54 +02:00
|
|
|
emit(state.copyWith(status: AttachmentsStatus.ready, selectedFiles: []));
|
2026-05-07 16:28:01 +02:00
|
|
|
} 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);
|
2026-05-09 11:43:54 +02:00
|
|
|
emit(state.copyWith(status: AttachmentsStatus.ready));
|
2026-05-07 16:28:01 +02:00
|
|
|
} 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));
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-18 12:00:07 +02:00
|
|
|
|
|
|
|
|
Bucket get _getBucketForParentType {
|
|
|
|
|
switch (state.parentType) {
|
|
|
|
|
case AttachmentParentType.customer:
|
|
|
|
|
return Bucket.documents;
|
|
|
|
|
case AttachmentParentType.ticket:
|
|
|
|
|
return Bucket.documents;
|
|
|
|
|
case AttachmentParentType.operation:
|
|
|
|
|
return Bucket.documents;
|
|
|
|
|
case AttachmentParentType.shippingDocument:
|
|
|
|
|
return Bucket.companyDocuments;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-07 16:28:01 +02:00
|
|
|
}
|