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/string_extensions.dart'; import 'package:flux/features/services/data/services_repository.dart'; import 'package:flux/features/services/models/energy_service_model.dart'; import 'package:flux/features/services/models/entertainment_service_model.dart'; import 'package:flux/features/services/models/fin_service_model.dart'; import 'package:flux/features/services/models/service_file_model.dart'; import 'package:flux/features/services/models/service_model.dart'; import 'package:get_it/get_it.dart'; import 'package:collection/collection.dart'; part 'services_state.dart'; class ServicesCubit extends Cubit { final ServicesRepository _repository = GetIt.I(); final SessionBloc _sessionBloc = GetIt.I(); ServicesCubit() : super(const ServicesState(status: ServicesStatus.initial)); // --- CARICAMENTO E PAGINAZIONE --- Future loadServices({bool refresh = false}) async { // Se stiamo già caricando, evitiamo chiamate doppie if (state.status == ServicesStatus.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, errorMessage: null, // Se è un refresh, svuotiamo la lista attuale per mostrare lo shimmer/loading allServices: refresh ? [] : state.allServices, hasReachedMax: refresh ? false : state.hasReachedMax, ), ); try { final currentOffset = refresh ? 0 : state.allServices.length; final companyId = _sessionBloc.state.company?.id; if (companyId == null) { throw Exception("Company ID non trovato nella sessione"); } final newServices = await _repository.fetchServices( 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 = newServices.length < 50; emit( state.copyWith( status: ServicesStatus.ready, allServices: refresh ? newServices : [...state.allServices, ...newServices], hasReachedMax: reachedMax, ), ); } catch (e) { emit( state.copyWith( status: ServicesStatus.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, ), ); loadServices(refresh: true); } /// Pulisce tutti i filtri void clearFilters() { emit(state.copyWith(query: '', dateRange: null)); loadServices(refresh: true); } // --- GESTIONE BOZZA (DRAFT) --- /// Inizializza un nuovo servizio o ne carica uno esistente per la modifica void initServiceForm({ ServiceModel? existingService, String? serviceId, }) async { if (existingService != null) { emit( state.copyWith( currentService: existingService, status: ServicesStatus.ready, ), ); } else if (serviceId != null) { ServiceModel? serviceModel = state.allServices.firstWhereOrNull( (s) => s.id == serviceId, ); serviceModel ??= await _repository.fetchServiceById(serviceId); emit( state.copyWith( currentService: serviceModel, status: ServicesStatus.ready, ), ); } else { // Crea un template vuoto con lo store di default (se disponibile) emit( state.copyWith( currentService: ServiceModel( storeId: _sessionBloc.state.selectedStore?.id ?? '', number: '', // Sarà compilato dall'utente createdAt: DateTime.now(), companyId: _sessionBloc.state.company!.id, ), status: ServicesStatus.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.currentService == null) return; final updated = state.currentService!.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(currentService: updated)); } // --- GESTIONE MODULI COMPLESSI --- void updateEnergyServices(List energyList) { emit( state.copyWith( currentService: state.currentService?.copyWith( energyServices: energyList, ), ), ); } void updateFinServices(List finList) { emit( state.copyWith( currentService: state.currentService?.copyWith(finServices: finList), ), ); } void updateEntertainmentServices(List entList) { emit( state.copyWith( currentService: state.currentService?.copyWith( entertainmentServices: entList, ), ), ); } // --- PERSISTENZA --- Future saveCurrentService({required bool isBozza}) async { if (state.currentService == null) return; emit(state.copyWith(status: ServicesStatus.saving, errorMessage: null)); try { // 1. Aggiorniamo il flag bozza in base a quale pulsante ha premuto l'utente final serviceToSave = state.currentService!.copyWith(isBozza: isBozza); // 2. Salvataggio corazzato await _repository.saveFullService(serviceToSave); // 3. Reset e ricaricamento emit(state.copyWith(status: ServicesStatus.saved, currentService: null)); await loadServices(refresh: true); } catch (e) { emit( state.copyWith( status: ServicesStatus.failure, errorMessage: e.toString(), ), ); } } // --- GESTIONE ALLEGATI LOCALI --- void addAttachments(List files) { final newAttachments = files.map((file) { return ServiceFileModel( id: null, // Meglio null se non è su DB serviceId: state.currentService?.id ?? '', name: file.name.fileNameWithoutExtension(), extension: file.name.fileExtension(), url: '', fileSize: file.size, localBytes: file.bytes, createdAt: DateTime.now(), ); }).toList(); // Creiamo una nuova lista pulita final List updatedList = [ ...(state.currentService?.files ?? []), ...newAttachments, ]; // Emettiamo lo stato assicurandoci che il ServiceModel venga clonato if (state.currentService != null) { emit( state.copyWith( currentService: state.currentService!.copyWith(files: updatedList), ), ); } } void removeAttachment(int index) { if (state.currentService == null) return; final updatedList = List.from( state.currentService!.files, ); updatedList.removeAt(index); emit( state.copyWith( currentService: state.currentService?.copyWith(files: updatedList), ), ); } void saveAndCopyFileToCustomer(ServiceFileModel file) async { final currentService = state.currentService; if (currentService == null || currentService.customerId == null) { // Magari mostra un errore: non posso copiare al cliente se non c'è un cliente! return; } emit(state.copyWith(status: ServicesStatus.loading)); try { // 1. Salviamo la pratica (Bozza o definitiva che sia) // Questo assicura che il file sia stato caricato su Storage e censito su DB await saveCurrentService(isBozza: currentService.isBozza); // 2. Recuperiamo il file "aggiornato" // Dopo il saveCurrentService, il file che prima era "locale" ora ha un URL. // Lo cerchiamo nella lista aggiornata per nome o estensione. final savedFile = state.currentService!.files.firstWhere( (f) => f.name == file.name && f.extension == file.extension, orElse: () => file, ); if (savedFile.url.isEmpty) { throw Exception( "Errore: URL del file non trovato dopo il salvataggio.", ); } // 3. Chiamiamo il repository per la copia fisica nel database del cliente // Passiamo l'URL del file e l'ID del cliente await _repository.copyFileToCustomer( file: savedFile, customerId: currentService.customerId!, ); // 4. Feedback all'utente // Potresti emettere un successo o mostrare un toast emit(state.copyWith(status: ServicesStatus.success)); } catch (e) { emit( state.copyWith( status: ServicesStatus.failure, errorMessage: "Errore durante la copia del file: $e", ), ); } } }