2026-04-16 11:50:29 +02:00
|
|
|
import 'package:equatable/equatable.dart';
|
2026-04-20 16:52:20 +02:00
|
|
|
import 'package:file_picker/file_picker.dart';
|
2026-04-16 11:50:29 +02:00
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
2026-04-20 23:52:00 +02:00
|
|
|
import 'package:flux/core/blocs/session/session_cubit.dart';
|
2026-04-20 16:52:20 +02:00
|
|
|
import 'package:flux/core/utils/string_extensions.dart';
|
2026-04-16 11:50:29 +02:00
|
|
|
import 'package:flux/features/services/data/services_repository.dart';
|
2026-04-20 16:52:20 +02:00
|
|
|
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';
|
2026-04-16 11:50:29 +02:00
|
|
|
import 'package:flux/features/services/models/service_model.dart';
|
|
|
|
|
import 'package:get_it/get_it.dart';
|
2026-04-20 16:52:20 +02:00
|
|
|
import 'package:collection/collection.dart';
|
|
|
|
|
part 'services_state.dart';
|
2026-04-16 11:50:29 +02:00
|
|
|
|
|
|
|
|
class ServicesCubit extends Cubit<ServicesState> {
|
|
|
|
|
final ServicesRepository _repository = GetIt.I<ServicesRepository>();
|
2026-04-21 11:26:42 +02:00
|
|
|
final SessionCubit _sessionCubit = GetIt.I<SessionCubit>();
|
2026-04-20 16:52:20 +02:00
|
|
|
|
|
|
|
|
ServicesCubit() : super(const ServicesState(status: ServicesStatus.initial));
|
2026-04-16 11:50:29 +02:00
|
|
|
|
2026-04-20 16:52:20 +02:00
|
|
|
// --- CARICAMENTO E PAGINAZIONE ---
|
2026-04-16 11:50:29 +02:00
|
|
|
|
|
|
|
|
Future<void> loadServices({bool refresh = false}) async {
|
2026-04-20 16:52:20 +02:00
|
|
|
// Se stiamo già caricando, evitiamo chiamate doppie
|
|
|
|
|
if (state.status == ServicesStatus.loading) return;
|
2026-04-16 11:50:29 +02:00
|
|
|
|
2026-04-20 16:52:20 +02:00
|
|
|
// Se non è un refresh e abbiamo già raggiunto la fine dei dati, ci fermiamo
|
|
|
|
|
if (!refresh && state.hasReachedMax) return;
|
2026-04-16 11:50:29 +02:00
|
|
|
|
|
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
2026-04-20 16:52:20 +02:00
|
|
|
status: ServicesStatus.loading,
|
|
|
|
|
errorMessage: null,
|
|
|
|
|
// Se è un refresh, svuotiamo la lista attuale per mostrare lo shimmer/loading
|
2026-04-16 11:50:29 +02:00
|
|
|
allServices: refresh ? [] : state.allServices,
|
|
|
|
|
hasReachedMax: refresh ? false : state.hasReachedMax,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
try {
|
2026-04-20 16:52:20 +02:00
|
|
|
final currentOffset = refresh ? 0 : state.allServices.length;
|
2026-04-21 11:26:42 +02:00
|
|
|
final companyId = _sessionCubit.state.company?.id;
|
2026-04-20 16:52:20 +02:00
|
|
|
|
|
|
|
|
if (companyId == null) {
|
|
|
|
|
throw Exception("Company ID non trovato nella sessione");
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-16 11:50:29 +02:00
|
|
|
final newServices = await _repository.fetchServices(
|
2026-04-20 16:52:20 +02:00
|
|
|
companyId: companyId,
|
2026-04-16 11:50:29 +02:00
|
|
|
offset: currentOffset,
|
2026-04-20 16:52:20 +02:00
|
|
|
limit: 50,
|
2026-04-16 11:50:29 +02:00
|
|
|
searchTerm: state.query,
|
|
|
|
|
dateRange: state.dateRange,
|
|
|
|
|
);
|
|
|
|
|
|
2026-04-20 16:52:20 +02:00
|
|
|
// Se ricevi meno record del limite, significa che non ce ne sono altri sul DB
|
|
|
|
|
final bool reachedMax = newServices.length < 50;
|
|
|
|
|
|
2026-04-16 11:50:29 +02:00
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
2026-04-20 16:52:20 +02:00
|
|
|
status: ServicesStatus.ready,
|
|
|
|
|
allServices: refresh
|
|
|
|
|
? newServices
|
|
|
|
|
: [...state.allServices, ...newServices],
|
|
|
|
|
hasReachedMax: reachedMax,
|
2026-04-16 11:50:29 +02:00
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
} catch (e) {
|
2026-04-20 16:52:20 +02:00
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
|
|
|
|
status: ServicesStatus.failure,
|
|
|
|
|
errorMessage: "Errore nel caricamento servizi: $e",
|
|
|
|
|
),
|
|
|
|
|
);
|
2026-04-16 11:50:29 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 16:52:20 +02:00
|
|
|
// --- GESTIONE FILTRI ---
|
|
|
|
|
|
|
|
|
|
/// Aggiorna i parametri di ricerca e ricarica da zero
|
2026-04-16 11:50:29 +02:00
|
|
|
void updateFilters({String? query, DateTimeRange? range}) {
|
2026-04-20 16:52:20 +02:00
|
|
|
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(
|
2026-04-21 11:26:42 +02:00
|
|
|
storeId: _sessionCubit.state.currentStore?.id ?? '',
|
2026-04-20 16:52:20 +02:00
|
|
|
number: '', // Sarà compilato dall'utente
|
|
|
|
|
createdAt: DateTime.now(),
|
2026-04-21 11:26:42 +02:00
|
|
|
companyId: _sessionCubit.state.company!.id!,
|
2026-04-20 16:52:20 +02:00
|
|
|
),
|
|
|
|
|
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<EnergyServiceModel> energyList) {
|
|
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
|
|
|
|
currentService: state.currentService?.copyWith(
|
|
|
|
|
energyServices: energyList,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
2026-04-16 11:50:29 +02:00
|
|
|
}
|
|
|
|
|
|
2026-04-20 16:52:20 +02:00
|
|
|
void updateFinServices(List<FinServiceModel> finList) {
|
|
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
|
|
|
|
currentService: state.currentService?.copyWith(finServices: finList),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void updateEntertainmentServices(List<EntertainmentServiceModel> entList) {
|
|
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
|
|
|
|
currentService: state.currentService?.copyWith(
|
|
|
|
|
entertainmentServices: entList,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- PERSISTENZA ---
|
|
|
|
|
|
|
|
|
|
Future<void> saveCurrentService({required bool isBozza}) async {
|
|
|
|
|
if (state.currentService == null) return;
|
|
|
|
|
|
|
|
|
|
emit(state.copyWith(status: ServicesStatus.saving, errorMessage: null));
|
2026-04-16 11:50:29 +02:00
|
|
|
try {
|
2026-04-20 16:52:20 +02:00
|
|
|
// 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);
|
2026-04-16 11:50:29 +02:00
|
|
|
} catch (e) {
|
2026-04-20 16:52:20 +02:00
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
|
|
|
|
status: ServicesStatus.failure,
|
|
|
|
|
errorMessage: e.toString(),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- GESTIONE ALLEGATI LOCALI ---
|
|
|
|
|
|
|
|
|
|
void addAttachments(List<PlatformFile> 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<ServiceFileModel> 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<ServiceFileModel>.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",
|
|
|
|
|
),
|
|
|
|
|
);
|
2026-04-16 11:50:29 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|