import 'package:equatable/equatable.dart'; import 'package:file_picker/file_picker.dart'; 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/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 'operations_state.dart'; class OperationsCubit extends Cubit { final OperationsRepository _repository = GetIt.I(); final SessionCubit _sessionCubit = GetIt.I(); OperationsCubit() : super(const OperationsState(status: OperationsStatus.initial)); // --- CARICAMENTO E PAGINAZIONE --- Future loadOperations({bool refresh = false}) async { // Se stiamo già caricando, evitiamo chiamate doppie 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: OperationsStatus.loading, errorMessage: null, // Se è un refresh, svuotiamo la lista attuale per mostrare lo shimmer/loading allOperations: refresh ? [] : state.allOperations, hasReachedMax: refresh ? false : state.hasReachedMax, ), ); try { 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 newOperations = await _repository.fetchOperations( companyId: companyId, offset: currentOffset, limit: 50, searchTerm: state.query, dateRange: state.dateRange, ); // Se ricevi meno record del limite, significa che non ce ne sono altri sul DB final bool reachedMax = newOperations.length < 50; emit( state.copyWith( status: OperationsStatus.ready, allOperations: refresh ? newOperations : [...state.allOperations, ...newOperations], hasReachedMax: reachedMax, ), ); } catch (e) { emit( state.copyWith( status: OperationsStatus.failure, errorMessage: "Errore nel caricamento servizi: $e", ), ); } } // --- GESTIONE FILTRI --- /// Aggiorna i parametri di ricerca e ricarica da zero void updateFilters({String? query, DateTimeRange? range}) { emit( state.copyWith( query: query ?? state.query, dateRange: range ?? state.dateRange, ), ); loadOperations(refresh: true); } /// Pulisce tutti i filtri void clearFilters() { emit(state.copyWith(query: '', dateRange: null)); loadOperations(refresh: true); } // --- GESTIONE BOZZA (DRAFT) --- /// Inizializza un nuovo servizio o ne carica uno esistente per la modifica void initOperationForm({ OperationModel? existingOperation, String? operationId, }) async { if (existingOperation != null) { emit( state.copyWith( currentOperation: existingOperation, status: OperationsStatus.ready, ), ); } else if (operationId != null) { OperationModel? operationModel = state.allOperations.firstWhereOrNull( (s) => s.id == operationId, ); operationModel ??= await _repository.fetchOperationById(operationId); emit( state.copyWith( currentOperation: operationModel, status: OperationsStatus.ready, ), ); } else { // Crea un template vuoto con lo store di default (se disponibile) emit( state.copyWith( currentOperation: OperationModel( storeId: _sessionCubit.state.currentStore?.id ?? '', number: '', // Sarà compilato dall'utente createdAt: DateTime.now(), companyId: _sessionCubit.state.company!.id!, ), status: OperationsStatus.ready, ), ); } } /// Metodo generico per aggiornare i campi base (AL, MNP, Note, ecc.) void updateField({ int? al, int? mnp, int? nip, int? unica, int? telepass, String? note, String? number, bool? isBozza, bool? resultOk, String? customerId, String? customerDisplayName, }) { if (state.currentOperation == null) return; final updated = state.currentOperation!.copyWith( al: al, mnp: mnp, nip: nip, unica: unica, telepass: telepass, note: note, number: number, isBozza: isBozza, resultOk: resultOk, customerId: customerId, customerDisplayName: customerDisplayName, ); emit(state.copyWith(currentOperation: updated)); } // --- GESTIONE MODULI COMPLESSI --- void updateEnergyOperations(List energyList) { emit( state.copyWith( currentOperation: state.currentOperation?.copyWith( energyOperations: energyList, ), ), ); } void updateFinOperations(List finList) { emit( state.copyWith( currentOperation: state.currentOperation?.copyWith( finOperations: finList, ), ), ); } void updateEntertainmentOperations( List entList, ) { emit( state.copyWith( currentOperation: state.currentOperation?.copyWith( entertainmentOperations: entList, ), ), ); } // --- PERSISTENZA --- Future saveCurrentOperation({ required bool isBozza, bool shouldPop = true, List? files, }) async { if (state.currentOperation == null) return; emit(state.copyWith(status: OperationsStatus.saving, errorMessage: null)); try { // 1. Aggiorniamo il flag bozza in base a quale pulsante ha premuto l'utente final operationToSave = state.currentOperation!.copyWith( isBozza: isBozza, files: files, ); // 2. Salvataggio corazzato final updatedOperation = await _repository.saveFullOperation( operationToSave, ); // 3. Reset e ricaricamento emit( state.copyWith( status: shouldPop ? OperationsStatus.saved : OperationsStatus.savedNoPop, currentOperation: shouldPop ? null : updatedOperation, ), ); await loadOperations(refresh: true); } catch (e) { emit( state.copyWith( status: OperationsStatus.failure, errorMessage: e.toString(), ), ); } } // --- GESTIONE ALLEGATI LOCALI --- void addAttachments(List files) { final newAttachments = files.map((file) { return OperationFileModel( id: null, // Meglio null se non è su DB operationId: state.currentOperation?.id ?? '', name: file.name.fileNameWithoutExtension(), extension: file.name.fileExtension(), storagePath: '', fileSize: file.size, localBytes: file.bytes, createdAt: DateTime.now(), ); }).toList(); // Creiamo una nuova lista pulita final List updatedList = [ ...(state.currentOperation?.files ?? []), ...newAttachments, ]; // Emettiamo lo stato assicurandoci che il OperationModel venga clonato if (state.currentOperation != null) { emit( state.copyWith( currentOperation: state.currentOperation!.copyWith( files: updatedList, ), ), ); } } void removeAttachment(int index) { if (state.currentOperation == null) return; final updatedList = List.from( state.currentOperation!.files, ); updatedList.removeAt(index); emit( state.copyWith( currentOperation: state.currentOperation?.copyWith(files: updatedList), ), ); } void saveAndCopyFileToCustomer(List selectedFiles) async { final currentOperation = state.currentOperation; // 1. Check di sicurezza: se non c'è il cliente, non sappiamo dove copiare if (currentOperation == null || currentOperation.customerId == null) { emit( state.copyWith( status: OperationsStatus.failure, errorMessage: "Impossibile copiare: nessun cliente associato alla pratica.", ), ); return; } 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 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 = updatedOperation.files.firstWhere( (f) => f.name == selectedFile.name && f.extension == selectedFile.extension, orElse: () => throw Exception( "File ${selectedFile.name} non trovato dopo il salvataggio.", ), ); // Creiamo il link nel database del cliente await _repository.copyFileToCustomer( file: persistedFile, customerId: currentOperation.customerId!, ); } // 4. AGGIORNAMENTO STATO // Aggiorniamo il Cubit con il servizio salvato così la UI mostra i file come "Remoti" emit( state.copyWith( status: OperationsStatus.success, currentOperation: updatedOperation, ), ); } catch (e) { emit( state.copyWith( status: OperationsStatus.failure, errorMessage: "Errore durante il salvataggio e copia: $e", ), ); } } }