This commit is contained in:
2026-05-01 10:11:44 +02:00
parent 9c8576ada5
commit f8bcac51e1
48 changed files with 1187 additions and 1141 deletions

View File

@@ -0,0 +1,242 @@
import 'dart:async';
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flux/core/utils/extensions.dart';
import 'package:flux/features/operations/data/operations_repository.dart';
import 'package:flux/features/operations/models/operation_file_model.dart';
import 'package:flux/features/operations/models/operation_model.dart';
import 'package:get_it/get_it.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<UploadMultipleOperationFilesEvent>(_onUploadMultipleOperationFiles);
on<DeleteOperationFilesEvent>(_onDeleteOperationFiles);
on<ToggleOperationFileSelectionEvent>(_onToggleOperationFileSelection);
// 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,
) {
// 1. Aggiorniamo l'ID nello stato
// 2. PIALLIAMO i file locali: ormai sono partiti per Supabase!
// Così la UI si pulisce all'istante e aspetta quelli remoti.
emit(
state.copyWith(
operationId: event.operationId,
localFiles: [], // <-- LA MAGIA ANTI-DUPLICATI
),
);
// Lanciamo il caricamento
add(LoadOperationFilesEvent(operationId: event.operationId));
}
FutureOr<void> _onLoadOperationFiles(
LoadOperationFilesEvent event,
Emitter<OperationFilesState> emit,
) async {
// Usiamo l'ID dell'evento, e se non c'è usiamo quello dello stato
final currentId = event.operationId ?? state.operationId;
if (currentId != null) {
emit(state.copyWith(status: OperationFilesStatus.loading));
await emit.forEach(
_repository.getOperationFilesStream(
currentId,
), // <-- Usiamo l'ID corretto!
onData: (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)
if (currentId == null) {
// Mettiamo i file nel "parcheggio" locale dello State
final newLocalFiles = event.files.map((file) {
return OperationFileModel(
id: null,
operationId: operationId ?? '',
name: file.name.fileNameWithoutExtension(),
extension: file.name.fileExtension(),
storagePath: '',
fileSize: file.size,
localBytes: file.bytes,
);
}).toList();
final List<OperationFileModel> updatedLocalFiles = [
...state.localFiles,
...newLocalFiles,
];
emit(
state.copyWith(
localFiles: updatedLocalFiles,
status: OperationFilesStatus.success,
),
);
return;
}
// BIVIO 2: PRATICA ESISTENTE (Abbiamo l'ID)
emit(state.copyWith(status: OperationFilesStatus.uploading));
try {
// Logica identica a quella che abbiamo fatto per i clienti
for (var file in event.files) {
await _repository.uploadAndRegisterOperationFile(
operationId: operationId!,
pickedFile: file,
);
}
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.photos == null) return;
if (event.pickedFiles!.isEmpty && event.photos!.isEmpty) return;
// BIVIO 2: PRATICA ESISTENTE (Abbiamo l'ID
emit(state.copyWith(status: OperationFilesStatus.uploading));
try {
// Logica identica a quella che abbiamo fatto per i clienti
if (event.pickedFiles != null && event.pickedFiles!.isNotEmpty) {
for (var file in event.pickedFiles!) {
await _repository.uploadAndRegisterOperationFile(
operationId: state.operationId!,
pickedFile: file,
);
}
}
emit(state.copyWith(status: OperationFilesStatus.success));
} catch (e) {
emit(
state.copyWith(
status: OperationFilesStatus.failure,
error: e.toString(),
),
);
}
}
FutureOr<void> _onUploadMultipleOperationFiles(
UploadMultipleOperationFilesEvent event,
Emitter<OperationFilesState> emit,
) async {
if (event.files.isEmpty) {
emit(
state.copyWith(
status: OperationFilesStatus.failure,
error: "Nessun file selezionato",
),
);
return;
}
emit(state.copyWith(status: OperationFilesStatus.uploading, error: null));
try {
// 2. Creiamo una lista di "Promesse" (Futures) per il repository
final List<Future<void>> uploadTasks = [];
for (var file in event.files) {
// Aggiungiamo il task alla lista, ma NON usiamo await qui dentro!
uploadTasks.add(
_repository.uploadAndRegisterOperationFile(
operationId: state.operationId!,
pickedFile: file,
),
);
}
// 3. ESECUZIONE PARALLELA!
// Aspettiamo che tutti i file siano caricati contemporaneamente.
await Future.wait(uploadTasks);
// 4. GRAN FINALE: Tutto caricato, emettiamo il success!
emit(state.copyWith(status: OperationFilesStatus.success));
} catch (e) {
// Se anche un solo file fallisce, catturiamo l'errore
emit(
state.copyWith(
status: OperationFilesStatus.failure,
error: "Errore durante l'upload multiplo: $e",
),
);
}
}
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,
) {
List<OperationFileModel> selectedFiles = List.from(state.selectedFiles);
if (selectedFiles.contains(event.file)) {
selectedFiles.remove(event.file);
} else {
selectedFiles.add(event.file);
}
emit(state.copyWith(selectedFiles: selectedFiles));
}
}

View File

@@ -0,0 +1,56 @@
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 OperationModel? 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<File>? photos;
const UploadOperationFilesEvent({this.pickedFiles, this.photos});
@override
List<Object?> get props => [pickedFiles, photos];
}
class UploadMultipleOperationFilesEvent extends OperationFilesEvent {
final List<PlatformFile> files;
const UploadMultipleOperationFilesEvent(this.files);
@override
List<Object?> get props => [files];
}
class DeleteOperationFilesEvent extends OperationFilesEvent {}
class ToggleOperationFileSelectionEvent extends OperationFilesEvent {
final OperationFileModel file;
const ToggleOperationFileSelectionEvent(this.file);
}

View File

@@ -0,0 +1,52 @@
part of 'operation_files_bloc.dart';
enum OperationFilesStatus { initial, loading, uploading, success, failure }
class OperationFilesState extends Equatable {
const OperationFilesState({
this.operationId,
required this.status,
this.error,
this.localFiles = const [],
this.remoteFiles = const [],
this.selectedFiles = const [],
});
final String? operationId;
final OperationFilesStatus status;
final String? error;
final List<OperationFileModel> localFiles;
final List<OperationFileModel> remoteFiles;
final List<OperationFileModel> selectedFiles;
@override
List<Object?> get props => [
operationId,
status,
error,
localFiles,
remoteFiles,
selectedFiles,
];
List<OperationFileModel> get allFiles => [...remoteFiles, ...localFiles];
OperationFilesState copyWith({
String? operationId,
OperationFilesStatus? status,
String? error,
List<OperationFileModel>? localFiles,
List<OperationFileModel>? remoteFiles,
List<OperationFileModel>? selectedFiles,
}) {
return OperationFilesState(
operationId: operationId ?? this.operationId,
status: status ?? this.status,
error: error,
localFiles: localFiles ?? this.localFiles,
remoteFiles: remoteFiles ?? this.remoteFiles,
selectedFiles: selectedFiles ?? this.selectedFiles,
);
}
}

View File

@@ -4,50 +4,51 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/blocs/session/session_cubit.dart';
import 'package:flux/core/utils/extensions.dart';
import 'package:flux/features/operations/data/services_repository.dart';
import 'package:flux/features/operations/models/energy_service_model.dart';
import 'package:flux/features/operations/models/entertainment_service_model.dart';
import 'package:flux/features/operations/models/fin_service_model.dart';
import 'package:flux/features/operations/models/service_file_model.dart';
import 'package:flux/features/operations/models/service_model.dart';
import 'package:flux/features/operations/data/operations_repository.dart';
import 'package:flux/features/operations/models/energy_operation_model.dart';
import 'package:flux/features/operations/models/entertainment_operation_model.dart';
import 'package:flux/features/operations/models/fin_operation_model.dart';
import 'package:flux/features/operations/models/operation_file_model.dart';
import 'package:flux/features/operations/models/operation_model.dart';
import 'package:get_it/get_it.dart';
import 'package:collection/collection.dart';
part 'services_state.dart';
part 'operations_state.dart';
class ServicesCubit extends Cubit<ServicesState> {
final ServicesRepository _repository = GetIt.I<ServicesRepository>();
class OperationsCubit extends Cubit<OperationsState> {
final OperationsRepository _repository = GetIt.I<OperationsRepository>();
final SessionCubit _sessionCubit = GetIt.I<SessionCubit>();
ServicesCubit() : super(const ServicesState(status: ServicesStatus.initial));
OperationsCubit()
: super(const OperationsState(status: OperationsStatus.initial));
// --- CARICAMENTO E PAGINAZIONE ---
Future<void> loadServices({bool refresh = false}) async {
Future<void> loadOperations({bool refresh = false}) async {
// Se stiamo già caricando, evitiamo chiamate doppie
if (state.status == ServicesStatus.loading) return;
if (state.status == OperationsStatus.loading) return;
// Se non è un refresh e abbiamo già raggiunto la fine dei dati, ci fermiamo
if (!refresh && state.hasReachedMax) return;
emit(
state.copyWith(
status: ServicesStatus.loading,
status: OperationsStatus.loading,
errorMessage: null,
// Se è un refresh, svuotiamo la lista attuale per mostrare lo shimmer/loading
allServices: refresh ? [] : state.allServices,
allOperations: refresh ? [] : state.allOperations,
hasReachedMax: refresh ? false : state.hasReachedMax,
),
);
try {
final currentOffset = refresh ? 0 : state.allServices.length;
final currentOffset = refresh ? 0 : state.allOperations.length;
final companyId = _sessionCubit.state.company?.id;
if (companyId == null) {
throw Exception("Company ID non trovato nella sessione");
}
final newServices = await _repository.fetchServices(
final newOperations = await _repository.fetchOperations(
companyId: companyId,
offset: currentOffset,
limit: 50,
@@ -56,21 +57,21 @@ class ServicesCubit extends Cubit<ServicesState> {
);
// Se ricevi meno record del limite, significa che non ce ne sono altri sul DB
final bool reachedMax = newServices.length < 50;
final bool reachedMax = newOperations.length < 50;
emit(
state.copyWith(
status: ServicesStatus.ready,
allServices: refresh
? newServices
: [...state.allServices, ...newServices],
status: OperationsStatus.ready,
allOperations: refresh
? newOperations
: [...state.allOperations, ...newOperations],
hasReachedMax: reachedMax,
),
);
} catch (e) {
emit(
state.copyWith(
status: ServicesStatus.failure,
status: OperationsStatus.failure,
errorMessage: "Errore nel caricamento servizi: $e",
),
);
@@ -87,51 +88,51 @@ class ServicesCubit extends Cubit<ServicesState> {
dateRange: range ?? state.dateRange,
),
);
loadServices(refresh: true);
loadOperations(refresh: true);
}
/// Pulisce tutti i filtri
void clearFilters() {
emit(state.copyWith(query: '', dateRange: null));
loadServices(refresh: true);
loadOperations(refresh: true);
}
// --- GESTIONE BOZZA (DRAFT) ---
/// Inizializza un nuovo servizio o ne carica uno esistente per la modifica
void initServiceForm({
ServiceModel? existingService,
String? serviceId,
void initOperationForm({
OperationModel? existingOperation,
String? operationId,
}) async {
if (existingService != null) {
if (existingOperation != null) {
emit(
state.copyWith(
currentService: existingService,
status: ServicesStatus.ready,
currentOperation: existingOperation,
status: OperationsStatus.ready,
),
);
} else if (serviceId != null) {
ServiceModel? serviceModel = state.allServices.firstWhereOrNull(
(s) => s.id == serviceId,
} else if (operationId != null) {
OperationModel? operationModel = state.allOperations.firstWhereOrNull(
(s) => s.id == operationId,
);
serviceModel ??= await _repository.fetchServiceById(serviceId);
operationModel ??= await _repository.fetchOperationById(operationId);
emit(
state.copyWith(
currentService: serviceModel,
status: ServicesStatus.ready,
currentOperation: operationModel,
status: OperationsStatus.ready,
),
);
} else {
// Crea un template vuoto con lo store di default (se disponibile)
emit(
state.copyWith(
currentService: ServiceModel(
currentOperation: OperationModel(
storeId: _sessionCubit.state.currentStore?.id ?? '',
number: '', // Sarà compilato dall'utente
createdAt: DateTime.now(),
companyId: _sessionCubit.state.company!.id!,
),
status: ServicesStatus.ready,
status: OperationsStatus.ready,
),
);
}
@@ -151,9 +152,9 @@ class ServicesCubit extends Cubit<ServicesState> {
String? customerId,
String? customerDisplayName,
}) {
if (state.currentService == null) return;
if (state.currentOperation == null) return;
final updated = state.currentService!.copyWith(
final updated = state.currentOperation!.copyWith(
al: al,
mnp: mnp,
nip: nip,
@@ -167,34 +168,38 @@ class ServicesCubit extends Cubit<ServicesState> {
customerDisplayName: customerDisplayName,
);
emit(state.copyWith(currentService: updated));
emit(state.copyWith(currentOperation: updated));
}
// --- GESTIONE MODULI COMPLESSI ---
void updateEnergyServices(List<EnergyServiceModel> energyList) {
void updateEnergyOperations(List<EnergyOperationModel> energyList) {
emit(
state.copyWith(
currentService: state.currentService?.copyWith(
energyServices: energyList,
currentOperation: state.currentOperation?.copyWith(
energyOperations: energyList,
),
),
);
}
void updateFinServices(List<FinServiceModel> finList) {
void updateFinOperations(List<FinOperationModel> finList) {
emit(
state.copyWith(
currentService: state.currentService?.copyWith(finServices: finList),
currentOperation: state.currentOperation?.copyWith(
finOperations: finList,
),
),
);
}
void updateEntertainmentServices(List<EntertainmentServiceModel> entList) {
void updateEntertainmentOperations(
List<EntertainmentOperationModel> entList,
) {
emit(
state.copyWith(
currentService: state.currentService?.copyWith(
entertainmentServices: entList,
currentOperation: state.currentOperation?.copyWith(
entertainmentOperations: entList,
),
),
);
@@ -202,36 +207,40 @@ class ServicesCubit extends Cubit<ServicesState> {
// --- PERSISTENZA ---
Future<void> saveCurrentService({
Future<void> saveCurrentOperation({
required bool isBozza,
bool shouldPop = true,
List<ServiceFileModel>? files,
List<OperationFileModel>? files,
}) async {
if (state.currentService == null) return;
if (state.currentOperation == null) return;
emit(state.copyWith(status: ServicesStatus.saving, errorMessage: null));
emit(state.copyWith(status: OperationsStatus.saving, errorMessage: null));
try {
// 1. Aggiorniamo il flag bozza in base a quale pulsante ha premuto l'utente
final serviceToSave = state.currentService!.copyWith(
final operationToSave = state.currentOperation!.copyWith(
isBozza: isBozza,
files: files,
);
// 2. Salvataggio corazzato
final updatedService = await _repository.saveFullService(serviceToSave);
final updatedOperation = await _repository.saveFullOperation(
operationToSave,
);
// 3. Reset e ricaricamento
emit(
state.copyWith(
status: shouldPop ? ServicesStatus.saved : ServicesStatus.savedNoPop,
currentService: shouldPop ? null : updatedService,
status: shouldPop
? OperationsStatus.saved
: OperationsStatus.savedNoPop,
currentOperation: shouldPop ? null : updatedOperation,
),
);
await loadServices(refresh: true);
await loadOperations(refresh: true);
} catch (e) {
emit(
state.copyWith(
status: ServicesStatus.failure,
status: OperationsStatus.failure,
errorMessage: e.toString(),
),
);
@@ -242,9 +251,9 @@ class ServicesCubit extends Cubit<ServicesState> {
void addAttachments(List<PlatformFile> files) {
final newAttachments = files.map((file) {
return ServiceFileModel(
return OperationFileModel(
id: null, // Meglio null se non è su DB
serviceId: state.currentService?.id ?? '',
operationId: state.currentOperation?.id ?? '',
name: file.name.fileNameWithoutExtension(),
extension: file.name.fileExtension(),
storagePath: '',
@@ -255,44 +264,46 @@ class ServicesCubit extends Cubit<ServicesState> {
}).toList();
// Creiamo una nuova lista pulita
final List<ServiceFileModel> updatedList = [
...(state.currentService?.files ?? []),
final List<OperationFileModel> updatedList = [
...(state.currentOperation?.files ?? []),
...newAttachments,
];
// Emettiamo lo stato assicurandoci che il ServiceModel venga clonato
if (state.currentService != null) {
// Emettiamo lo stato assicurandoci che il OperationModel venga clonato
if (state.currentOperation != null) {
emit(
state.copyWith(
currentService: state.currentService!.copyWith(files: updatedList),
currentOperation: state.currentOperation!.copyWith(
files: updatedList,
),
),
);
}
}
void removeAttachment(int index) {
if (state.currentService == null) return;
if (state.currentOperation == null) return;
final updatedList = List<ServiceFileModel>.from(
state.currentService!.files,
final updatedList = List<OperationFileModel>.from(
state.currentOperation!.files,
);
updatedList.removeAt(index);
emit(
state.copyWith(
currentService: state.currentService?.copyWith(files: updatedList),
currentOperation: state.currentOperation?.copyWith(files: updatedList),
),
);
}
void saveAndCopyFileToCustomer(List<ServiceFileModel> selectedFiles) async {
final currentService = state.currentService;
void saveAndCopyFileToCustomer(List<OperationFileModel> selectedFiles) async {
final currentOperation = state.currentOperation;
// 1. Check di sicurezza: se non c'è il cliente, non sappiamo dove copiare
if (currentService == null || currentService.customerId == null) {
if (currentOperation == null || currentOperation.customerId == null) {
emit(
state.copyWith(
status: ServicesStatus.failure,
status: OperationsStatus.failure,
errorMessage:
"Impossibile copiare: nessun cliente associato alla pratica.",
),
@@ -300,19 +311,21 @@ class ServicesCubit extends Cubit<ServicesState> {
return;
}
emit(state.copyWith(status: ServicesStatus.loading));
emit(state.copyWith(status: OperationsStatus.loading));
try {
// 2. SALVATAGGIO CORAZZATO
// Chiamiamo il repo e otteniamo la pratica con TUTTI i file ora dotati di ID e storagePath
final updatedService = await _repository.saveFullService(currentService);
final updatedOperation = await _repository.saveFullOperation(
currentOperation,
);
// 3. COPIA RELAZIONALE
// Per ogni file che l'utente ha selezionato nella UI, cerchiamo la sua versione
// "ufficiale" (quella con lo storagePath) nel modello appena tornato dal DB.
for (var selectedFile in selectedFiles) {
// Cerchiamo il match nel modello aggiornato
final persistedFile = updatedService.files.firstWhere(
final persistedFile = updatedOperation.files.firstWhere(
(f) =>
f.name == selectedFile.name &&
f.extension == selectedFile.extension,
@@ -324,7 +337,7 @@ class ServicesCubit extends Cubit<ServicesState> {
// Creiamo il link nel database del cliente
await _repository.copyFileToCustomer(
file: persistedFile,
customerId: currentService.customerId!,
customerId: currentOperation.customerId!,
);
}
@@ -332,14 +345,14 @@ class ServicesCubit extends Cubit<ServicesState> {
// Aggiorniamo il Cubit con il servizio salvato così la UI mostra i file come "Remoti"
emit(
state.copyWith(
status: ServicesStatus.success,
currentService: updatedService,
status: OperationsStatus.success,
currentOperation: updatedOperation,
),
);
} catch (e) {
emit(
state.copyWith(
status: ServicesStatus.failure,
status: OperationsStatus.failure,
errorMessage: "Errore durante il salvataggio e copia: $e",
),
);

View File

@@ -1,6 +1,6 @@
part of 'operations_cubit.dart';
enum ServicesStatus {
enum OperationsStatus {
initial,
loading,
ready,
@@ -11,20 +11,20 @@ enum ServicesStatus {
failure,
}
class ServicesState extends Equatable {
final ServicesStatus status;
final List<ServiceModel> allServices;
final ServiceModel? currentService; // La bozza che stiamo editando
class OperationsState extends Equatable {
final OperationsStatus status;
final List<OperationModel> allOperations;
final OperationModel? currentOperation; // La bozza che stiamo editando
final String? errorMessage;
final String query;
final DateTimeRange? dateRange;
final bool hasReachedMax;
final bool isSavingDraft;
const ServicesState({
const OperationsState({
required this.status,
this.allServices = const [],
this.currentService,
this.allOperations = const [],
this.currentOperation,
this.errorMessage,
this.query = '',
this.dateRange,
@@ -32,20 +32,20 @@ class ServicesState extends Equatable {
this.isSavingDraft = false,
});
ServicesState copyWith({
ServicesStatus? status,
List<ServiceModel>? allServices,
ServiceModel? currentService,
OperationsState copyWith({
OperationsStatus? status,
List<OperationModel>? allOperations,
OperationModel? currentOperation,
String? errorMessage,
String? query,
DateTimeRange? dateRange,
bool? hasReachedMax,
bool? isSavingDraft,
}) {
return ServicesState(
return OperationsState(
status: status ?? this.status,
allServices: allServices ?? this.allServices,
currentService: currentService ?? this.currentService,
allOperations: allOperations ?? this.allOperations,
currentOperation: currentOperation ?? this.currentOperation,
errorMessage: errorMessage,
query: query ?? this.query,
dateRange: dateRange ?? this.dateRange,
@@ -57,8 +57,8 @@ class ServicesState extends Equatable {
@override
List<Object?> get props => [
status,
allServices,
currentService,
allOperations,
currentOperation,
errorMessage,
query,
dateRange,

View File

@@ -1,232 +0,0 @@
import 'dart:async';
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flux/core/utils/extensions.dart';
import 'package:flux/features/operations/data/services_repository.dart';
import 'package:flux/features/operations/models/service_file_model.dart';
import 'package:flux/features/operations/models/service_model.dart';
import 'package:get_it/get_it.dart';
part 'service_files_events.dart';
part 'service_files_state.dart';
class ServiceFilesBloc extends Bloc<ServiceFilesEvent, ServiceFilesState> {
final _repository = GetIt.I.get<ServicesRepository>();
final String? serviceId;
ServiceFilesBloc({this.serviceId})
: super(
ServiceFilesState(
status: ServiceFilesStatus.initial,
serviceId: serviceId,
),
) {
on<ServiceSavedEvent>(_onServiceSaved);
on<LoadServiceFilesEvent>(_onLoadServiceFiles);
on<AddServiceFilesEvent>(_onAddServiceFiles);
on<UploadServiceFilesEvent>(_onUploadServiceFiles);
on<UploadMultipleServiceFilesEvent>(_onUploadMultipleServiceFiles);
on<DeleteServiceFilesEvent>(_onDeleteServiceFiles);
on<ToggleServiceFileSelectionEvent>(_onToggleServiceFileSelection);
// Se il BLoC nasce con un ID, accendiamo subito lo stream!
if (serviceId != null) {
add(LoadServiceFilesEvent(serviceId: serviceId));
}
}
FutureOr<void> _onServiceSaved(
ServiceSavedEvent event,
Emitter<ServiceFilesState> emit,
) {
// 1. Aggiorniamo l'ID nello stato
// 2. PIALLIAMO i file locali: ormai sono partiti per Supabase!
// Così la UI si pulisce all'istante e aspetta quelli remoti.
emit(
state.copyWith(
serviceId: event.serviceId,
localFiles: [], // <-- LA MAGIA ANTI-DUPLICATI
),
);
// Lanciamo il caricamento
add(LoadServiceFilesEvent(serviceId: event.serviceId));
}
FutureOr<void> _onLoadServiceFiles(
LoadServiceFilesEvent event,
Emitter<ServiceFilesState> emit,
) async {
// Usiamo l'ID dell'evento, e se non c'è usiamo quello dello stato
final currentId = event.serviceId ?? state.serviceId;
if (currentId != null) {
emit(state.copyWith(status: ServiceFilesStatus.loading));
await emit.forEach(
_repository.getServiceFilesStream(
currentId,
), // <-- Usiamo l'ID corretto!
onData: (data) => state.copyWith(
status: ServiceFilesStatus.success,
remoteFiles: data,
),
onError: (error, stackTrace) => state.copyWith(
status: ServiceFilesStatus.failure,
error: error.toString(),
),
);
}
}
void _onAddServiceFiles(
AddServiceFilesEvent event,
Emitter<ServiceFilesState> emit,
) async {
final currentId = state.serviceId;
// BIVIO 1: PRATICA NUOVA (Nessun ID)
if (currentId == null) {
// Mettiamo i file nel "parcheggio" locale dello State
final newLocalFiles = event.files.map((file) {
return ServiceFileModel(
id: null,
serviceId: serviceId ?? '',
name: file.name.fileNameWithoutExtension(),
extension: file.name.fileExtension(),
storagePath: '',
fileSize: file.size,
localBytes: file.bytes,
);
}).toList();
final List<ServiceFileModel> updatedLocalFiles = [
...state.localFiles,
...newLocalFiles,
];
emit(
state.copyWith(
localFiles: updatedLocalFiles,
status: ServiceFilesStatus.success,
),
);
return;
}
// BIVIO 2: PRATICA ESISTENTE (Abbiamo l'ID)
emit(state.copyWith(status: ServiceFilesStatus.uploading));
try {
// Logica identica a quella che abbiamo fatto per i clienti
for (var file in event.files) {
await _repository.uploadAndRegisterServiceFile(
serviceId: serviceId!,
pickedFile: file,
);
}
emit(state.copyWith(status: ServiceFilesStatus.success));
} catch (e) {
emit(
state.copyWith(status: ServiceFilesStatus.failure, error: e.toString()),
);
}
}
FutureOr<void> _onUploadServiceFiles(
UploadServiceFilesEvent event,
Emitter<ServiceFilesState> emit,
) async {
if (event.pickedFiles == null && event.photos == null) return;
if (event.pickedFiles!.isEmpty && event.photos!.isEmpty) return;
// BIVIO 2: PRATICA ESISTENTE (Abbiamo l'ID
emit(state.copyWith(status: ServiceFilesStatus.uploading));
try {
// Logica identica a quella che abbiamo fatto per i clienti
if (event.pickedFiles != null && event.pickedFiles!.isNotEmpty) {
for (var file in event.pickedFiles!) {
await _repository.uploadAndRegisterServiceFile(
serviceId: state.serviceId!,
pickedFile: file,
);
}
}
emit(state.copyWith(status: ServiceFilesStatus.success));
} catch (e) {
emit(
state.copyWith(status: ServiceFilesStatus.failure, error: e.toString()),
);
}
}
FutureOr<void> _onUploadMultipleServiceFiles(
UploadMultipleServiceFilesEvent event,
Emitter<ServiceFilesState> emit,
) async {
if (event.files.isEmpty) {
emit(
state.copyWith(
status: ServiceFilesStatus.failure,
error: "Nessun file selezionato",
),
);
return;
}
emit(state.copyWith(status: ServiceFilesStatus.uploading, error: null));
try {
// 2. Creiamo una lista di "Promesse" (Futures) per il repository
final List<Future<void>> uploadTasks = [];
for (var file in event.files) {
// Aggiungiamo il task alla lista, ma NON usiamo await qui dentro!
uploadTasks.add(
_repository.uploadAndRegisterServiceFile(
serviceId: state.serviceId!,
pickedFile: file,
),
);
}
// 3. ESECUZIONE PARALLELA!
// Aspettiamo che tutti i file siano caricati contemporaneamente.
await Future.wait(uploadTasks);
// 4. GRAN FINALE: Tutto caricato, emettiamo il success!
emit(state.copyWith(status: ServiceFilesStatus.success));
} catch (e) {
// Se anche un solo file fallisce, catturiamo l'errore
emit(
state.copyWith(
status: ServiceFilesStatus.failure,
error: "Errore durante l'upload multiplo: $e",
),
);
}
}
FutureOr<void> _onDeleteServiceFiles(
DeleteServiceFilesEvent event,
Emitter<ServiceFilesState> emit,
) async {
emit(state.copyWith(status: ServiceFilesStatus.loading));
try {
await _repository.deleteServiceFiles(state.selectedFiles);
emit(
state.copyWith(status: ServiceFilesStatus.success, selectedFiles: []),
);
} catch (e) {
emit(
state.copyWith(status: ServiceFilesStatus.failure, error: e.toString()),
);
}
}
FutureOr<void> _onToggleServiceFileSelection(
ToggleServiceFileSelectionEvent event,
Emitter<ServiceFilesState> emit,
) {
List<ServiceFileModel> selectedFiles = List.from(state.selectedFiles);
if (selectedFiles.contains(event.file)) {
selectedFiles.remove(event.file);
} else {
selectedFiles.add(event.file);
}
emit(state.copyWith(selectedFiles: selectedFiles));
}
}

View File

@@ -1,56 +0,0 @@
part of 'service_files_bloc.dart';
abstract class ServiceFilesEvent extends Equatable {
const ServiceFilesEvent();
@override
List<Object?> get props => [];
}
class ServiceSavedEvent extends ServiceFilesEvent {
final String serviceId;
const ServiceSavedEvent(this.serviceId);
@override
List<Object?> get props => [serviceId];
}
class LoadServiceFilesEvent extends ServiceFilesEvent {
final String? serviceId;
final ServiceModel? operation;
const LoadServiceFilesEvent({this.serviceId, this.operation});
@override
List<Object?> get props => [serviceId, operation];
}
class AddServiceFilesEvent extends ServiceFilesEvent {
final List<PlatformFile> files;
const AddServiceFilesEvent(this.files);
@override
List<Object?> get props => [files];
}
class UploadServiceFilesEvent extends ServiceFilesEvent {
final List<PlatformFile>? pickedFiles;
final List<File>? photos;
const UploadServiceFilesEvent({this.pickedFiles, this.photos});
@override
List<Object?> get props => [pickedFiles, photos];
}
class UploadMultipleServiceFilesEvent extends ServiceFilesEvent {
final List<PlatformFile> files;
const UploadMultipleServiceFilesEvent(this.files);
@override
List<Object?> get props => [files];
}
class DeleteServiceFilesEvent extends ServiceFilesEvent {}
class ToggleServiceFileSelectionEvent extends ServiceFilesEvent {
final ServiceFileModel file;
const ToggleServiceFileSelectionEvent(this.file);
}

View File

@@ -1,52 +0,0 @@
part of 'service_files_bloc.dart';
enum ServiceFilesStatus { initial, loading, uploading, success, failure }
class ServiceFilesState extends Equatable {
const ServiceFilesState({
this.serviceId,
required this.status,
this.error,
this.localFiles = const [],
this.remoteFiles = const [],
this.selectedFiles = const [],
});
final String? serviceId;
final ServiceFilesStatus status;
final String? error;
final List<ServiceFileModel> localFiles;
final List<ServiceFileModel> remoteFiles;
final List<ServiceFileModel> selectedFiles;
@override
List<Object?> get props => [
serviceId,
status,
error,
localFiles,
remoteFiles,
selectedFiles,
];
List<ServiceFileModel> get allFiles => [...remoteFiles, ...localFiles];
ServiceFilesState copyWith({
String? serviceId,
ServiceFilesStatus? status,
String? error,
List<ServiceFileModel>? localFiles,
List<ServiceFileModel>? remoteFiles,
List<ServiceFileModel>? selectedFiles,
}) {
return ServiceFilesState(
serviceId: serviceId ?? this.serviceId,
status: status ?? this.status,
error: error,
localFiles: localFiles ?? this.localFiles,
remoteFiles: remoteFiles ?? this.remoteFiles,
selectedFiles: selectedFiles ?? this.selectedFiles,
);
}
}