feat-tickets #14
@@ -24,7 +24,7 @@ import 'package:flux/features/master_data/staff/ui/staff_screen.dart';
|
|||||||
import 'package:flux/features/master_data/store/ui/stores_screen.dart';
|
import 'package:flux/features/master_data/store/ui/stores_screen.dart';
|
||||||
import 'package:flux/features/onboarding/blocs/onboarding_cubit.dart';
|
import 'package:flux/features/onboarding/blocs/onboarding_cubit.dart';
|
||||||
import 'package:flux/features/onboarding/ui/onboarding_screen.dart';
|
import 'package:flux/features/onboarding/ui/onboarding_screen.dart';
|
||||||
import 'package:flux/features/attachments/blocs/operation_files_bloc.dart';
|
import 'package:flux/features/attachments/blocs/attachments_bloc.dart';
|
||||||
import 'package:flux/features/operations/models/operation_model.dart';
|
import 'package:flux/features/operations/models/operation_model.dart';
|
||||||
import 'package:flux/features/operations/ui/operation_form_screen.dart';
|
import 'package:flux/features/operations/ui/operation_form_screen.dart';
|
||||||
import 'package:flux/features/operations/ui/operation_mobile_upload_screen.dart';
|
import 'package:flux/features/operations/ui/operation_mobile_upload_screen.dart';
|
||||||
@@ -203,8 +203,9 @@ class AppRouter {
|
|||||||
context.read<StaffCubit>().loadStaffForStore(currentStoreId);
|
context.read<StaffCubit>().loadStaffForStore(currentStoreId);
|
||||||
|
|
||||||
return BlocProvider(
|
return BlocProvider(
|
||||||
create: (context) => OperationFilesBloc(
|
create: (context) => AttachmentsBloc(
|
||||||
operationId: operationId ?? existingOperation?.id,
|
parentId: operationId ?? existingOperation?.id,
|
||||||
|
parentType: AttachmentParentType.operation,
|
||||||
),
|
),
|
||||||
child: OperationFormScreen(
|
child: OperationFormScreen(
|
||||||
operationId: operationId ?? existingOperation?.id,
|
operationId: operationId ?? existingOperation?.id,
|
||||||
@@ -232,7 +233,10 @@ class AppRouter {
|
|||||||
context.read<ProductsCubit>().loadBrands();
|
context.read<ProductsCubit>().loadBrands();
|
||||||
context.read<StaffCubit>().loadStaffForStore(currentStoreId);
|
context.read<StaffCubit>().loadStaffForStore(currentStoreId);
|
||||||
return BlocProvider(
|
return BlocProvider(
|
||||||
create: (context) => OperationFilesBloc(operationId: operationId),
|
create: (context) => AttachmentsBloc(
|
||||||
|
parentId: operationId,
|
||||||
|
parentType: AttachmentParentType.operation,
|
||||||
|
),
|
||||||
child: OperationMobileUploadScreen(
|
child: OperationMobileUploadScreen(
|
||||||
operationId: operationId,
|
operationId: operationId,
|
||||||
operationName: operationName,
|
operationName: operationName,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import 'package:file_picker/file_picker.dart';
|
|||||||
import 'package:flux/features/attachments/data/attachments_repository.dart';
|
import 'package:flux/features/attachments/data/attachments_repository.dart';
|
||||||
import 'package:flux/features/attachments/ui/attachment_viewer_screen.dart';
|
import 'package:flux/features/attachments/ui/attachment_viewer_screen.dart';
|
||||||
import 'package:flux/features/attachments/ui/quick_rename_dialog.dart';
|
import 'package:flux/features/attachments/ui/quick_rename_dialog.dart';
|
||||||
import 'package:flux/features/attachments/blocs/operation_files_bloc.dart';
|
import 'package:flux/features/attachments/blocs/attachments_bloc.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:flux/features/operations/models/operation_model.dart';
|
import 'package:flux/features/operations/models/operation_model.dart';
|
||||||
@@ -88,16 +88,14 @@ class _OperationFilesSectionState extends State<OperationFilesSection> {
|
|||||||
|
|
||||||
if (result != null && mounted) {
|
if (result != null && mounted) {
|
||||||
// MAGIA: Passiamo direttamente la lista di PlatformFile al tuo BLoC!
|
// MAGIA: Passiamo direttamente la lista di PlatformFile al tuo BLoC!
|
||||||
context.read<OperationFilesBloc>().add(
|
context.read<AttachmentsBloc>().add(AddAttachmentsEvent(result.files));
|
||||||
AddOperationFilesEvent(result.files),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- APERTURA VIEWER ---
|
// --- APERTURA VIEWER ---
|
||||||
void _openFile(AttachmentModel file) {
|
void _openFile(AttachmentModel file) {
|
||||||
// 1. Catturiamo il BLoC dalla pagina corrente prima di navigare
|
// 1. Catturiamo il BLoC dalla pagina corrente prima di navigare
|
||||||
final operationFilesBloc = context.read<OperationFilesBloc>();
|
final operationFilesBloc = context.read<AttachmentsBloc>();
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
@@ -107,10 +105,10 @@ class _OperationFilesSectionState extends State<OperationFilesSection> {
|
|||||||
attachment: file,
|
attachment: file,
|
||||||
onRename: (newName) {
|
onRename: (newName) {
|
||||||
// Spara l'evento al BLoC e lui farà il resto!
|
// Spara l'evento al BLoC e lui farà il resto!
|
||||||
operationFilesBloc.add(RenameOperationFileEvent(file, newName));
|
operationFilesBloc.add(RenameAttachmentEvent(file, newName));
|
||||||
},
|
},
|
||||||
onDelete: () {
|
onDelete: () {
|
||||||
operationFilesBloc.add(DeleteSpecificOperationFileEvent(file));
|
operationFilesBloc.add(DeleteSpecificAttachmentEvent(file));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -392,7 +390,7 @@ class _OperationFilesSectionState extends State<OperationFilesSection> {
|
|||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
// USIAMO IL TUO BLOC!
|
// USIAMO IL TUO BLOC!
|
||||||
return BlocBuilder<OperationFilesBloc, OperationFilesState>(
|
return BlocBuilder<AttachmentsBloc, AttachmentsState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final allFiles = state.allFiles;
|
final allFiles = state.allFiles;
|
||||||
final selectedFiles = state.selectedFiles;
|
final selectedFiles = state.selectedFiles;
|
||||||
@@ -442,7 +440,7 @@ class _OperationFilesSectionState extends State<OperationFilesSection> {
|
|||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
icon: const Icon(Icons.add_photo_alternate),
|
icon: const Icon(Icons.add_photo_alternate),
|
||||||
label: const Text('Aggiungi File'),
|
label: const Text('Aggiungi File'),
|
||||||
onPressed: state.status == OperationFilesStatus.uploading
|
onPressed: state.status == AttachmentsStatus.uploading
|
||||||
? null
|
? null
|
||||||
: _pickFiles,
|
: _pickFiles,
|
||||||
),
|
),
|
||||||
@@ -463,12 +461,12 @@ class _OperationFilesSectionState extends State<OperationFilesSection> {
|
|||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (selectedFiles.length == allFiles.length) {
|
if (selectedFiles.length == allFiles.length) {
|
||||||
context.read<OperationFilesBloc>().add(
|
context.read<AttachmentsBloc>().add(
|
||||||
ClearOperationFileSelectionEvent(),
|
ClearAttachmentSelectionEvent(),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
context.read<OperationFilesBloc>().add(
|
context.read<AttachmentsBloc>().add(
|
||||||
SelectAllOperationFilesEvent(),
|
SelectAllAttachmentsEvent(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -477,7 +475,7 @@ class _OperationFilesSectionState extends State<OperationFilesSection> {
|
|||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
|
|
||||||
// Loader di upload
|
// Loader di upload
|
||||||
if (state.status == OperationFilesStatus.uploading)
|
if (state.status == AttachmentsStatus.uploading)
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: 24,
|
width: 24,
|
||||||
height: 24,
|
height: 24,
|
||||||
@@ -493,8 +491,8 @@ class _OperationFilesSectionState extends State<OperationFilesSection> {
|
|||||||
icon: const Icon(Icons.delete, color: Colors.red),
|
icon: const Icon(Icons.delete, color: Colors.red),
|
||||||
tooltip: 'Elimina selezionati',
|
tooltip: 'Elimina selezionati',
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.read<OperationFilesBloc>().add(
|
context.read<AttachmentsBloc>().add(
|
||||||
DeleteOperationFilesEvent(),
|
DeleteAttachmentsEvent(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -505,9 +503,10 @@ class _OperationFilesSectionState extends State<OperationFilesSection> {
|
|||||||
icon: const Icon(Icons.person_add, color: Colors.blue),
|
icon: const Icon(Icons.person_add, color: Colors.blue),
|
||||||
tooltip: 'Copia nei documenti del Cliente',
|
tooltip: 'Copia nei documenti del Cliente',
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.read<OperationFilesBloc>().add(
|
context.read<AttachmentsBloc>().add(
|
||||||
LinkFilesToCustomerEvent(
|
LinkAttachmentsToEntityEvent(
|
||||||
customerId: widget.currentOp.customerId!,
|
targetId: widget.currentOp.customerId!,
|
||||||
|
targetType: AttachmentParentType.customer,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
@@ -621,8 +620,8 @@ class _OperationFilesSectionState extends State<OperationFilesSection> {
|
|||||||
onTap: () => _openFile(file),
|
onTap: () => _openFile(file),
|
||||||
onLongPress: () {
|
onLongPress: () {
|
||||||
// Selezione rapida con long press!
|
// Selezione rapida con long press!
|
||||||
context.read<OperationFilesBloc>().add(
|
context.read<AttachmentsBloc>().add(
|
||||||
ToggleOperationFileSelectionEvent(file),
|
ToggleAttachmentSelectionEvent(file),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
@@ -696,8 +695,8 @@ class _OperationFilesSectionState extends State<OperationFilesSection> {
|
|||||||
right: 4,
|
right: 4,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.read<OperationFilesBloc>().add(
|
context.read<AttachmentsBloc>().add(
|
||||||
ToggleOperationFileSelectionEvent(file),
|
ToggleAttachmentSelectionEvent(file),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
|
|||||||
391
lib/features/attachments/blocs/attachments_bloc.dart
Normal file
391
lib/features/attachments/blocs/attachments_bloc.dart
Normal file
@@ -0,0 +1,391 @@
|
|||||||
|
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>();
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}).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 companyId = GetIt.I.get<SessionCubit>().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<Future<void>> 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<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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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<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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
68
lib/features/attachments/blocs/attachments_events.dart
Normal file
68
lib/features/attachments/blocs/attachments_events.dart
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
part of 'attachments_bloc.dart';
|
||||||
|
|
||||||
|
abstract class AttachmentsEvent extends Equatable {
|
||||||
|
const AttachmentsEvent();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Chiamato quando l'entità "padre" (es. il Ticket) viene salvata per la prima volta
|
||||||
|
class ParentEntitySavedEvent extends AttachmentsEvent {
|
||||||
|
final String newParentId;
|
||||||
|
const ParentEntitySavedEvent(this.newParentId);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [newParentId];
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoadAttachmentsEvent extends AttachmentsEvent {
|
||||||
|
final String? parentId;
|
||||||
|
const LoadAttachmentsEvent({this.parentId});
|
||||||
|
}
|
||||||
|
|
||||||
|
class AddAttachmentsEvent extends AttachmentsEvent {
|
||||||
|
final List<PlatformFile> files;
|
||||||
|
const AddAttachmentsEvent(this.files);
|
||||||
|
}
|
||||||
|
|
||||||
|
class UploadAttachmentsEvent extends AttachmentsEvent {
|
||||||
|
final List<PlatformFile>? pickedFiles;
|
||||||
|
final List<XFile>? photos;
|
||||||
|
const UploadAttachmentsEvent({this.pickedFiles, this.photos});
|
||||||
|
}
|
||||||
|
|
||||||
|
class DeleteAttachmentsEvent extends AttachmentsEvent {}
|
||||||
|
|
||||||
|
class ToggleAttachmentSelectionEvent extends AttachmentsEvent {
|
||||||
|
final AttachmentModel file;
|
||||||
|
const ToggleAttachmentSelectionEvent(this.file);
|
||||||
|
}
|
||||||
|
|
||||||
|
class SelectAllAttachmentsEvent extends AttachmentsEvent {}
|
||||||
|
|
||||||
|
class ClearAttachmentSelectionEvent extends AttachmentsEvent {}
|
||||||
|
|
||||||
|
class LinkAttachmentsToEntityEvent extends AttachmentsEvent {
|
||||||
|
final AttachmentParentType targetType;
|
||||||
|
final String targetId;
|
||||||
|
|
||||||
|
const LinkAttachmentsToEntityEvent({
|
||||||
|
required this.targetType,
|
||||||
|
required this.targetId,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [targetType, targetId];
|
||||||
|
}
|
||||||
|
|
||||||
|
class RenameAttachmentEvent extends AttachmentsEvent {
|
||||||
|
final AttachmentModel file;
|
||||||
|
final String newName;
|
||||||
|
const RenameAttachmentEvent(this.file, this.newName);
|
||||||
|
}
|
||||||
|
|
||||||
|
class DeleteSpecificAttachmentEvent extends AttachmentsEvent {
|
||||||
|
final AttachmentModel file;
|
||||||
|
const DeleteSpecificAttachmentEvent(this.file);
|
||||||
|
}
|
||||||
@@ -1,10 +1,28 @@
|
|||||||
part of 'operation_files_bloc.dart';
|
part of 'attachments_bloc.dart';
|
||||||
|
|
||||||
enum OperationFilesStatus { initial, loading, uploading, success, failure }
|
enum AttachmentsStatus { initial, loading, uploading, success, failure }
|
||||||
|
|
||||||
class OperationFilesState extends Equatable {
|
enum AttachmentParentType {
|
||||||
const OperationFilesState({
|
operation('operation_id'),
|
||||||
this.operationId,
|
ticket('ticket_id'),
|
||||||
|
customer('customer_id');
|
||||||
|
|
||||||
|
final String dbColumn;
|
||||||
|
const AttachmentParentType(this.dbColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
class AttachmentsState extends Equatable {
|
||||||
|
final String? parentId;
|
||||||
|
final AttachmentParentType parentType;
|
||||||
|
final AttachmentsStatus status;
|
||||||
|
final String? error;
|
||||||
|
final List<AttachmentModel> localFiles;
|
||||||
|
final List<AttachmentModel> remoteFiles;
|
||||||
|
final List<AttachmentModel> selectedFiles;
|
||||||
|
|
||||||
|
const AttachmentsState({
|
||||||
|
this.parentId,
|
||||||
|
required this.parentType,
|
||||||
required this.status,
|
required this.status,
|
||||||
this.error,
|
this.error,
|
||||||
this.localFiles = const [],
|
this.localFiles = const [],
|
||||||
@@ -12,17 +30,10 @@ class OperationFilesState extends Equatable {
|
|||||||
this.selectedFiles = const [],
|
this.selectedFiles = const [],
|
||||||
});
|
});
|
||||||
|
|
||||||
final String? operationId;
|
|
||||||
final OperationFilesStatus status;
|
|
||||||
final String? error;
|
|
||||||
final List<AttachmentModel> localFiles;
|
|
||||||
final List<AttachmentModel> remoteFiles;
|
|
||||||
|
|
||||||
final List<AttachmentModel> selectedFiles;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [
|
List<Object?> get props => [
|
||||||
operationId,
|
parentId,
|
||||||
|
parentType,
|
||||||
status,
|
status,
|
||||||
error,
|
error,
|
||||||
localFiles,
|
localFiles,
|
||||||
@@ -32,16 +43,18 @@ class OperationFilesState extends Equatable {
|
|||||||
|
|
||||||
List<AttachmentModel> get allFiles => [...remoteFiles, ...localFiles];
|
List<AttachmentModel> get allFiles => [...remoteFiles, ...localFiles];
|
||||||
|
|
||||||
OperationFilesState copyWith({
|
AttachmentsState copyWith({
|
||||||
String? operationId,
|
String? parentId,
|
||||||
OperationFilesStatus? status,
|
AttachmentParentType? parentType,
|
||||||
|
AttachmentsStatus? status,
|
||||||
String? error,
|
String? error,
|
||||||
List<AttachmentModel>? localFiles,
|
List<AttachmentModel>? localFiles,
|
||||||
List<AttachmentModel>? remoteFiles,
|
List<AttachmentModel>? remoteFiles,
|
||||||
List<AttachmentModel>? selectedFiles,
|
List<AttachmentModel>? selectedFiles,
|
||||||
}) {
|
}) {
|
||||||
return OperationFilesState(
|
return AttachmentsState(
|
||||||
operationId: operationId ?? this.operationId,
|
parentId: parentId ?? this.parentId,
|
||||||
|
parentType: parentType ?? this.parentType,
|
||||||
status: status ?? this.status,
|
status: status ?? this.status,
|
||||||
error: error,
|
error: error,
|
||||||
localFiles: localFiles ?? this.localFiles,
|
localFiles: localFiles ?? this.localFiles,
|
||||||
@@ -1,389 +0,0 @@
|
|||||||
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<OperationFilesEvent, OperationFilesState> {
|
|
||||||
final _repository = GetIt.I.get<OperationsRepository>();
|
|
||||||
final String? operationId;
|
|
||||||
|
|
||||||
OperationFilesBloc({this.operationId})
|
|
||||||
: super(
|
|
||||||
OperationFilesState(
|
|
||||||
status: OperationFilesStatus.initial,
|
|
||||||
operationId: operationId,
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
on<OperationsavedEvent>(_onOperationsaved);
|
|
||||||
on<LoadOperationFilesEvent>(_onLoadOperationFiles);
|
|
||||||
on<AddOperationFilesEvent>(_onAddOperationFiles);
|
|
||||||
on<UploadOperationFilesEvent>(_onUploadOperationFiles);
|
|
||||||
on<DeleteOperationFilesEvent>(_onDeleteOperationFiles);
|
|
||||||
on<ToggleOperationFileSelectionEvent>(_onToggleOperationFileSelection);
|
|
||||||
on<LinkFilesToCustomerEvent>(_onLinkFilesToCustomer);
|
|
||||||
on<RenameOperationFileEvent>(_onRenameOperationFile);
|
|
||||||
on<DeleteSpecificOperationFileEvent>(_onDeleteSpecificOperationFiles);
|
|
||||||
on<SelectAllOperationFilesEvent>(_onSelectAllOperationFiles);
|
|
||||||
on<ClearOperationFileSelectionEvent>(_onClearOperationFileSelection);
|
|
||||||
|
|
||||||
// Se il BLoC nasce con un ID, accendiamo subito lo stream!
|
|
||||||
if (operationId != null) {
|
|
||||||
add(LoadOperationFilesEvent(operationId: operationId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FutureOr<void> _onOperationsaved(
|
|
||||||
OperationsavedEvent event,
|
|
||||||
Emitter<OperationFilesState> 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<Future<void>> 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<void> _onLoadOperationFiles(
|
|
||||||
LoadOperationFilesEvent event,
|
|
||||||
Emitter<OperationFilesState> 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<AttachmentModel> data) => state.copyWith(
|
|
||||||
status: OperationFilesStatus.success,
|
|
||||||
remoteFiles: data,
|
|
||||||
),
|
|
||||||
onError: (error, stackTrace) => state.copyWith(
|
|
||||||
status: OperationFilesStatus.failure,
|
|
||||||
error: error.toString(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onAddOperationFiles(
|
|
||||||
AddOperationFilesEvent event,
|
|
||||||
Emitter<OperationFilesState> emit,
|
|
||||||
) async {
|
|
||||||
final currentId = state.operationId;
|
|
||||||
|
|
||||||
// BIVIO 1: PRATICA NUOVA (Nessun ID - salvataggio locale)
|
|
||||||
if (currentId == null) {
|
|
||||||
final companyId = GetIt.I.get<SessionCubit>().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<Future<void>> 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<void> _onUploadOperationFiles(
|
|
||||||
UploadOperationFilesEvent event,
|
|
||||||
Emitter<OperationFilesState> 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<Future<void>> 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<void> _onDeleteOperationFiles(
|
|
||||||
DeleteOperationFilesEvent event,
|
|
||||||
Emitter<OperationFilesState> 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<void> _onToggleOperationFileSelection(
|
|
||||||
ToggleOperationFileSelectionEvent event,
|
|
||||||
Emitter<OperationFilesState> 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 _onSelectAllOperationFiles(
|
|
||||||
SelectAllOperationFilesEvent event,
|
|
||||||
Emitter<OperationFilesState> emit,
|
|
||||||
) {
|
|
||||||
// Prendiamo TUTTI i file (locali e remoti) e li buttiamo nei selezionati
|
|
||||||
emit(state.copyWith(selectedFiles: state.allFiles));
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onClearOperationFileSelection(
|
|
||||||
ClearOperationFileSelectionEvent event,
|
|
||||||
Emitter<OperationFilesState> emit,
|
|
||||||
) {
|
|
||||||
// Svuotiamo brutalmente la lista
|
|
||||||
emit(state.copyWith(selectedFiles: []));
|
|
||||||
}
|
|
||||||
|
|
||||||
FutureOr<void> _onLinkFilesToCustomer(
|
|
||||||
LinkFilesToCustomerEvent event,
|
|
||||||
Emitter<OperationFilesState> 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<Future<void>> 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",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FutureOr<void> _onRenameOperationFile(
|
|
||||||
RenameOperationFileEvent event,
|
|
||||||
Emitter<OperationFilesState> 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: OperationFilesStatus.loading));
|
|
||||||
try {
|
|
||||||
await _repository.renameAttachment(event.file.id!, event.newName);
|
|
||||||
emit(state.copyWith(status: OperationFilesStatus.success));
|
|
||||||
} catch (e) {
|
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
status: OperationFilesStatus.failure,
|
|
||||||
error: "Errore rinomina: $e",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FutureOr<void> _onDeleteSpecificOperationFiles(
|
|
||||||
DeleteSpecificOperationFileEvent event,
|
|
||||||
Emitter<OperationFilesState> emit,
|
|
||||||
) {
|
|
||||||
if (event.file.localBytes != null) {
|
|
||||||
final updatedLocalFiles = state.localFiles
|
|
||||||
.where((f) => f != event.file)
|
|
||||||
.toList();
|
|
||||||
emit(state.copyWith(localFiles: updatedLocalFiles));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
part of 'operation_files_bloc.dart';
|
|
||||||
|
|
||||||
abstract class OperationFilesEvent extends Equatable {
|
|
||||||
const OperationFilesEvent();
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object?> get props => [];
|
|
||||||
}
|
|
||||||
|
|
||||||
class OperationsavedEvent extends OperationFilesEvent {
|
|
||||||
final String operationId;
|
|
||||||
const OperationsavedEvent(this.operationId);
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object?> get props => [operationId];
|
|
||||||
}
|
|
||||||
|
|
||||||
class LoadOperationFilesEvent extends OperationFilesEvent {
|
|
||||||
final String? operationId;
|
|
||||||
final AttachmentModel? operation;
|
|
||||||
const LoadOperationFilesEvent({this.operationId, this.operation});
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object?> get props => [operationId, operation];
|
|
||||||
}
|
|
||||||
|
|
||||||
class AddOperationFilesEvent extends OperationFilesEvent {
|
|
||||||
final List<PlatformFile> files;
|
|
||||||
const AddOperationFilesEvent(this.files);
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object?> get props => [files];
|
|
||||||
}
|
|
||||||
|
|
||||||
class UploadOperationFilesEvent extends OperationFilesEvent {
|
|
||||||
final List<PlatformFile>? pickedFiles;
|
|
||||||
final List<XFile>? photos;
|
|
||||||
const UploadOperationFilesEvent({this.pickedFiles, this.photos});
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object?> get props => [pickedFiles, photos];
|
|
||||||
}
|
|
||||||
|
|
||||||
class LinkFilesToCustomerEvent extends OperationFilesEvent {
|
|
||||||
final String customerId;
|
|
||||||
|
|
||||||
const LinkFilesToCustomerEvent({required this.customerId});
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object?> get props => [customerId];
|
|
||||||
}
|
|
||||||
|
|
||||||
class DeleteOperationFilesEvent extends OperationFilesEvent {}
|
|
||||||
|
|
||||||
class ToggleOperationFileSelectionEvent extends OperationFilesEvent {
|
|
||||||
final AttachmentModel file;
|
|
||||||
const ToggleOperationFileSelectionEvent(this.file);
|
|
||||||
}
|
|
||||||
|
|
||||||
class RenameOperationFileEvent extends OperationFilesEvent {
|
|
||||||
final AttachmentModel file;
|
|
||||||
final String newName;
|
|
||||||
|
|
||||||
const RenameOperationFileEvent(this.file, this.newName);
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object?> get props => [file, newName];
|
|
||||||
}
|
|
||||||
|
|
||||||
class DeleteSpecificOperationFileEvent extends OperationFilesEvent {
|
|
||||||
final AttachmentModel file;
|
|
||||||
|
|
||||||
const DeleteSpecificOperationFileEvent(this.file);
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object?> get props => [file];
|
|
||||||
}
|
|
||||||
|
|
||||||
class SelectAllOperationFilesEvent extends OperationFilesEvent {}
|
|
||||||
|
|
||||||
class ClearOperationFileSelectionEvent extends OperationFilesEvent {}
|
|
||||||
@@ -1,23 +1,198 @@
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
import 'package:flux/features/attachments/models/attachment_model.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
import 'package:flux/features/attachments/blocs/attachments_bloc.dart';
|
||||||
|
import 'package:get_it/get_it.dart';
|
||||||
|
import 'package:flux/core/blocs/session/session_cubit.dart';
|
||||||
|
|
||||||
class AttachmentsRepository {
|
class AttachmentsRepository {
|
||||||
final _supabase = Supabase.instance.client;
|
final _supabase = Supabase.instance.client;
|
||||||
|
static const String _bucketName = 'attachments';
|
||||||
|
static const String _tableName =
|
||||||
|
'attachment'; // Cambia col vero nome della tua tabella se diverso!
|
||||||
|
|
||||||
/// Scarica i byte di un file direttamente da Supabase Storage
|
/// Scarica i byte di un file direttamente da Supabase Storage
|
||||||
Future<Uint8List> downloadAttachmentBytes(String storagePath) async {
|
Future<Uint8List> downloadAttachmentBytes(String storagePath) async {
|
||||||
try {
|
try {
|
||||||
// ATTENZIONE: Sostituisci 'attachments' con il nome VERO del tuo bucket su Supabase!
|
|
||||||
// Se il tuo storagePath contiene già il nome del bucket all'inizio,
|
|
||||||
// assicurati di passargli solo il percorso interno.
|
|
||||||
final Uint8List bytes = await _supabase.storage
|
final Uint8List bytes = await _supabase.storage
|
||||||
.from('attachments') // <--- NOME DEL TUO BUCKET
|
.from(_bucketName)
|
||||||
.download(storagePath);
|
.download(storagePath);
|
||||||
|
|
||||||
return bytes;
|
return bytes;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw Exception("Impossibile scaricare il documento dal cloud: $e");
|
throw Exception("Impossibile scaricare il documento dal cloud: $e");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// RESTITUISCE IL NOME DELLA COLONNA DB IN BASE AL TIPO
|
||||||
|
String _getColumnNameForParent(AttachmentParentType parentType) {
|
||||||
|
switch (parentType) {
|
||||||
|
case AttachmentParentType.operation:
|
||||||
|
return 'operation_id';
|
||||||
|
case AttachmentParentType.ticket:
|
||||||
|
return 'ticket_id';
|
||||||
|
case AttachmentParentType.customer:
|
||||||
|
return 'customer_id';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// RECUPERA I FILE IN TEMPO REALE
|
||||||
|
Stream<List<AttachmentModel>> getFilesStream(
|
||||||
|
String parentId,
|
||||||
|
AttachmentParentType parentType,
|
||||||
|
) {
|
||||||
|
final columnName = _getColumnNameForParent(parentType);
|
||||||
|
|
||||||
|
return _supabase
|
||||||
|
.from(_tableName)
|
||||||
|
.stream(primaryKey: ['id'])
|
||||||
|
.eq(columnName, parentId)
|
||||||
|
.map(
|
||||||
|
(listOfMaps) =>
|
||||||
|
listOfMaps.map((map) => AttachmentModel.fromMap(map)).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// CARICA IL FILE NELLO STORAGE E LO REGISTRA NEL DB
|
||||||
|
Future<void> uploadAndRegisterFile({
|
||||||
|
required String parentId,
|
||||||
|
required AttachmentParentType parentType,
|
||||||
|
required PlatformFile pickedFile,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final companyId = GetIt.I.get<SessionCubit>().state.company!.id!;
|
||||||
|
final extension = pickedFile.extension ?? pickedFile.name.split('.').last;
|
||||||
|
final cleanName = pickedFile.name
|
||||||
|
.replaceAll(RegExp(r'[^\w\s\.-]'), '')
|
||||||
|
.replaceAll(' ', '_');
|
||||||
|
|
||||||
|
// Creiamo un path ordinato: idAzienda/tipoEntita/idEntita/timestamp_nomefile
|
||||||
|
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
final storagePath =
|
||||||
|
'$companyId/${parentType.name}/$parentId/${timestamp}_$cleanName';
|
||||||
|
|
||||||
|
// 1. Upload su Supabase Storage
|
||||||
|
await _supabase.storage
|
||||||
|
.from(_bucketName)
|
||||||
|
.uploadBinary(
|
||||||
|
storagePath,
|
||||||
|
pickedFile.bytes!,
|
||||||
|
fileOptions: FileOptions(
|
||||||
|
upsert: true,
|
||||||
|
contentType: _guessContentType(extension),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Creiamo la mappa per il DB dinamicamente
|
||||||
|
final Map<String, dynamic> insertData = {
|
||||||
|
'company_id': companyId,
|
||||||
|
'name': pickedFile.name.replaceAll('.$extension', ''),
|
||||||
|
'extension': extension,
|
||||||
|
'file_size': pickedFile.size,
|
||||||
|
'storage_path': storagePath,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Inseriamo l'ID nella colonna giusta!
|
||||||
|
final columnName = _getColumnNameForParent(parentType);
|
||||||
|
insertData[columnName] = parentId;
|
||||||
|
|
||||||
|
// 3. Salviamo su Postgres
|
||||||
|
await _supabase.from(_tableName).insert(insertData);
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception("Errore nel caricamento del file: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ELIMINA IL FILE (Scollegamento intelligente)
|
||||||
|
Future<void> deleteFiles({
|
||||||
|
required List<AttachmentModel> files,
|
||||||
|
required AttachmentParentType currentContextType,
|
||||||
|
}) async {
|
||||||
|
if (files.isEmpty) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (var file in files) {
|
||||||
|
if (file.id == null) continue;
|
||||||
|
|
||||||
|
// 1. Capiamo quali collegamenti ha questo file attualmente
|
||||||
|
final currentLinks = {
|
||||||
|
AttachmentParentType.operation: file.operationId,
|
||||||
|
AttachmentParentType.ticket: file.ticketId,
|
||||||
|
AttachmentParentType.customer: file.customerId,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. Simuliamo la rimozione del collegamento per il contesto attuale
|
||||||
|
currentLinks[currentContextType] = null;
|
||||||
|
|
||||||
|
// 3. Controlliamo se rimangono altri ID valorizzati
|
||||||
|
final hasOtherActiveLinks = currentLinks.values.any(
|
||||||
|
(id) => id != null && id.isNotEmpty,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasOtherActiveLinks) {
|
||||||
|
// A. Ci sono ancora altre entità che usano questo file!
|
||||||
|
// Scolleghiamolo SOLO dal contesto attuale mettendo a NULL la sua colonna
|
||||||
|
await _supabase
|
||||||
|
.from(_tableName)
|
||||||
|
.update({currentContextType.dbColumn: null})
|
||||||
|
.eq('id', file.id!);
|
||||||
|
} else {
|
||||||
|
// B. Nessuno usa più questo file! ELIMINAZIONE FISICA TOTALE.
|
||||||
|
await _supabase.from(_tableName).delete().eq('id', file.id!);
|
||||||
|
|
||||||
|
if (file.storagePath != null) {
|
||||||
|
await _supabase.storage.from(_bucketName).remove([
|
||||||
|
file.storagePath!,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception("Errore nell'eliminazione dei file: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// RINOMINA UN FILE (Solo nel DB, non cambiamo il file fisico)
|
||||||
|
Future<void> renameAttachment(String fileId, String newName) async {
|
||||||
|
try {
|
||||||
|
await _supabase
|
||||||
|
.from(_tableName)
|
||||||
|
.update({'name': newName})
|
||||||
|
.eq('id', fileId);
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception("Errore nella rinomina del file: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ASSOCIA UN FILE A UN'ALTRA ENTITÀ (Modifica il record esistente)
|
||||||
|
Future<void> linkFileToEntity({
|
||||||
|
required String fileId,
|
||||||
|
required AttachmentParentType targetType,
|
||||||
|
required String targetId,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
// Facciamo un semplice UPDATE aggiungendo l'ID nella colonna giusta
|
||||||
|
await _supabase
|
||||||
|
.from(_tableName)
|
||||||
|
.update({targetType.dbColumn: targetId})
|
||||||
|
.eq('id', fileId);
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception("Errore nel collegamento del file: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper per indovinare il content-type base
|
||||||
|
String _guessContentType(String extension) {
|
||||||
|
switch (extension.toLowerCase()) {
|
||||||
|
case 'pdf':
|
||||||
|
return 'application/pdf';
|
||||||
|
case 'png':
|
||||||
|
return 'image/png';
|
||||||
|
case 'jpg':
|
||||||
|
case 'jpeg':
|
||||||
|
return 'image/jpeg';
|
||||||
|
default:
|
||||||
|
return 'application/octet-stream';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ class AttachmentModel extends Equatable {
|
|||||||
final DateTime? createdAt;
|
final DateTime? createdAt;
|
||||||
final String? customerId;
|
final String? customerId;
|
||||||
final String? operationId;
|
final String? operationId;
|
||||||
|
final String? ticketId;
|
||||||
final String name;
|
final String name;
|
||||||
final String extension;
|
final String extension;
|
||||||
final String? storagePath;
|
final String? storagePath;
|
||||||
@@ -19,6 +20,7 @@ class AttachmentModel extends Equatable {
|
|||||||
this.createdAt,
|
this.createdAt,
|
||||||
this.customerId,
|
this.customerId,
|
||||||
this.operationId,
|
this.operationId,
|
||||||
|
this.ticketId,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.extension,
|
required this.extension,
|
||||||
this.storagePath,
|
this.storagePath,
|
||||||
@@ -33,6 +35,7 @@ class AttachmentModel extends Equatable {
|
|||||||
createdAt,
|
createdAt,
|
||||||
customerId,
|
customerId,
|
||||||
operationId,
|
operationId,
|
||||||
|
ticketId,
|
||||||
name,
|
name,
|
||||||
extension,
|
extension,
|
||||||
storagePath,
|
storagePath,
|
||||||
@@ -59,6 +62,7 @@ class AttachmentModel extends Equatable {
|
|||||||
DateTime? createdAt,
|
DateTime? createdAt,
|
||||||
String? customerId,
|
String? customerId,
|
||||||
String? operationId,
|
String? operationId,
|
||||||
|
String? ticketId,
|
||||||
String? name,
|
String? name,
|
||||||
String? extension,
|
String? extension,
|
||||||
String? storagePath,
|
String? storagePath,
|
||||||
@@ -70,6 +74,7 @@ class AttachmentModel extends Equatable {
|
|||||||
createdAt: createdAt ?? this.createdAt,
|
createdAt: createdAt ?? this.createdAt,
|
||||||
customerId: customerId ?? this.customerId,
|
customerId: customerId ?? this.customerId,
|
||||||
operationId: operationId ?? this.operationId,
|
operationId: operationId ?? this.operationId,
|
||||||
|
ticketId: ticketId ?? this.ticketId,
|
||||||
name: name ?? this.name,
|
name: name ?? this.name,
|
||||||
extension: extension ?? this.extension,
|
extension: extension ?? this.extension,
|
||||||
storagePath: storagePath ?? this.storagePath,
|
storagePath: storagePath ?? this.storagePath,
|
||||||
@@ -86,6 +91,7 @@ class AttachmentModel extends Equatable {
|
|||||||
: null,
|
: null,
|
||||||
customerId: map['customer_id'] as String?,
|
customerId: map['customer_id'] as String?,
|
||||||
operationId: map['operation_id'] as String?,
|
operationId: map['operation_id'] as String?,
|
||||||
|
ticketId: map['ticket_id'] as String?,
|
||||||
name: map['name'] as String,
|
name: map['name'] as String,
|
||||||
extension: map['extension'] as String,
|
extension: map['extension'] as String,
|
||||||
storagePath: map['storage_path'] as String?,
|
storagePath: map['storage_path'] as String?,
|
||||||
@@ -104,6 +110,7 @@ class AttachmentModel extends Equatable {
|
|||||||
'storage_path': storagePath,
|
'storage_path': storagePath,
|
||||||
'customer_id': customerId,
|
'customer_id': customerId,
|
||||||
'operation_id': operationId,
|
'operation_id': operationId,
|
||||||
|
'ticket_id': ticketId,
|
||||||
'file_size': fileSize,
|
'file_size': fileSize,
|
||||||
'company_id': companyId,
|
'company_id': companyId,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flux/features/attachments/blocs/operation_files_bloc.dart';
|
import 'package:flux/features/attachments/blocs/attachments_bloc.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
|
||||||
@@ -36,10 +36,10 @@ class _OperationMobileUploadScreenState
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocListener<OperationFilesBloc, OperationFilesState>(
|
return BlocListener<AttachmentsBloc, AttachmentsState>(
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
// Quando il BLoC ci dice che ha finito l'upload (Success), chiudiamo la pagina!
|
// Quando il BLoC ci dice che ha finito l'upload (Success), chiudiamo la pagina!
|
||||||
if (state.status == OperationFilesStatus.success && _isUploading) {
|
if (state.status == AttachmentsStatus.success && _isUploading) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
const SnackBar(
|
||||||
content: Text("Tutti i file caricati con successo! ✅"),
|
content: Text("Tutti i file caricati con successo! ✅"),
|
||||||
@@ -47,7 +47,7 @@ class _OperationMobileUploadScreenState
|
|||||||
);
|
);
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
}
|
}
|
||||||
if (state.status == OperationFilesStatus.failure) {
|
if (state.status == AttachmentsStatus.failure) {
|
||||||
setState(() => _isUploading = false);
|
setState(() => _isUploading = false);
|
||||||
ScaffoldMessenger.of(
|
ScaffoldMessenger.of(
|
||||||
context,
|
context,
|
||||||
@@ -295,8 +295,8 @@ class _OperationMobileUploadScreenState
|
|||||||
|
|
||||||
// Diciamo al BLoC di caricare tutti i file.
|
// Diciamo al BLoC di caricare tutti i file.
|
||||||
// Usiamo il tuo evento esistente per ogni file (il BLoC li metterà in coda)
|
// Usiamo il tuo evento esistente per ogni file (il BLoC li metterà in coda)
|
||||||
final bloc = context.read<OperationFilesBloc>();
|
final bloc = context.read<AttachmentsBloc>();
|
||||||
bloc.add(UploadOperationFilesEvent(pickedFiles: _stagedFiles));
|
bloc.add(UploadAttachmentsEvent(pickedFiles: _stagedFiles));
|
||||||
|
|
||||||
// N.B: Il Navigator.pop() viene chiamato dal BlocListener in alto quando lo stato diventa "success"!
|
// N.B: Il Navigator.pop() viene chiamato dal BlocListener in alto quando lo stato diventa "success"!
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user