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-22 11:06:02 +02:00
|
|
|
import 'package:flux/core/blocs/session/session_cubit.dart';
|
2026-04-29 19:25:48 +02:00
|
|
|
import 'package:flux/core/utils/extensions.dart';
|
2026-05-01 10:11:44 +02:00
|
|
|
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';
|
2026-04-16 11:50:29 +02:00
|
|
|
import 'package:get_it/get_it.dart';
|
2026-04-20 16:52:20 +02:00
|
|
|
import 'package:collection/collection.dart';
|
2026-05-01 10:11:44 +02:00
|
|
|
part 'operations_state.dart';
|
2026-04-16 11:50:29 +02:00
|
|
|
|
2026-05-01 10:11:44 +02:00
|
|
|
class OperationsCubit extends Cubit<OperationsState> {
|
|
|
|
|
final OperationsRepository _repository = GetIt.I<OperationsRepository>();
|
2026-04-22 11:06:02 +02:00
|
|
|
final SessionCubit _sessionCubit = GetIt.I<SessionCubit>();
|
2026-04-20 16:52:20 +02:00
|
|
|
|
2026-05-01 10:11:44 +02:00
|
|
|
OperationsCubit()
|
|
|
|
|
: super(const OperationsState(status: OperationsStatus.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
|
|
|
|
2026-05-01 10:11:44 +02:00
|
|
|
Future<void> loadOperations({bool refresh = false}) async {
|
2026-04-20 16:52:20 +02:00
|
|
|
// Se stiamo già caricando, evitiamo chiamate doppie
|
2026-05-01 10:11:44 +02:00
|
|
|
if (state.status == OperationsStatus.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-05-01 10:11:44 +02:00
|
|
|
status: OperationsStatus.loading,
|
2026-04-20 16:52:20 +02:00
|
|
|
errorMessage: null,
|
|
|
|
|
// Se è un refresh, svuotiamo la lista attuale per mostrare lo shimmer/loading
|
2026-05-01 10:11:44 +02:00
|
|
|
allOperations: refresh ? [] : state.allOperations,
|
2026-04-16 11:50:29 +02:00
|
|
|
hasReachedMax: refresh ? false : state.hasReachedMax,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
try {
|
2026-05-01 10:11:44 +02:00
|
|
|
final currentOffset = refresh ? 0 : state.allOperations.length;
|
2026-04-22 11:06:02 +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-05-01 10:11:44 +02:00
|
|
|
final newOperations = await _repository.fetchOperations(
|
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
|
2026-05-01 10:11:44 +02:00
|
|
|
final bool reachedMax = newOperations.length < 50;
|
2026-04-20 16:52:20 +02:00
|
|
|
|
2026-04-16 11:50:29 +02:00
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
2026-05-01 10:11:44 +02:00
|
|
|
status: OperationsStatus.ready,
|
|
|
|
|
allOperations: refresh
|
|
|
|
|
? newOperations
|
|
|
|
|
: [...state.allOperations, ...newOperations],
|
2026-04-20 16:52:20 +02:00
|
|
|
hasReachedMax: reachedMax,
|
2026-04-16 11:50:29 +02:00
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
} catch (e) {
|
2026-04-20 16:52:20 +02:00
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
2026-05-01 10:11:44 +02:00
|
|
|
status: OperationsStatus.failure,
|
2026-04-20 16:52:20 +02:00
|
|
|
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,
|
|
|
|
|
),
|
|
|
|
|
);
|
2026-05-01 10:11:44 +02:00
|
|
|
loadOperations(refresh: true);
|
2026-04-20 16:52:20 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Pulisce tutti i filtri
|
|
|
|
|
void clearFilters() {
|
|
|
|
|
emit(state.copyWith(query: '', dateRange: null));
|
2026-05-01 10:11:44 +02:00
|
|
|
loadOperations(refresh: true);
|
2026-04-20 16:52:20 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- GESTIONE BOZZA (DRAFT) ---
|
|
|
|
|
|
|
|
|
|
/// Inizializza un nuovo servizio o ne carica uno esistente per la modifica
|
2026-05-01 10:11:44 +02:00
|
|
|
void initOperationForm({
|
|
|
|
|
OperationModel? existingOperation,
|
|
|
|
|
String? operationId,
|
2026-04-20 16:52:20 +02:00
|
|
|
}) async {
|
2026-05-01 10:11:44 +02:00
|
|
|
if (existingOperation != null) {
|
2026-04-20 16:52:20 +02:00
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
2026-05-01 10:11:44 +02:00
|
|
|
currentOperation: existingOperation,
|
|
|
|
|
status: OperationsStatus.ready,
|
2026-04-20 16:52:20 +02:00
|
|
|
),
|
|
|
|
|
);
|
2026-05-01 10:11:44 +02:00
|
|
|
} else if (operationId != null) {
|
|
|
|
|
OperationModel? operationModel = state.allOperations.firstWhereOrNull(
|
|
|
|
|
(s) => s.id == operationId,
|
2026-04-20 16:52:20 +02:00
|
|
|
);
|
2026-05-01 10:11:44 +02:00
|
|
|
operationModel ??= await _repository.fetchOperationById(operationId);
|
2026-04-20 16:52:20 +02:00
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
2026-05-01 10:11:44 +02:00
|
|
|
currentOperation: operationModel,
|
|
|
|
|
status: OperationsStatus.ready,
|
2026-04-20 16:52:20 +02:00
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
// Crea un template vuoto con lo store di default (se disponibile)
|
|
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
2026-05-01 10:11:44 +02:00
|
|
|
currentOperation: OperationModel(
|
2026-04-22 11:06:02 +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-22 11:06:02 +02:00
|
|
|
companyId: _sessionCubit.state.company!.id!,
|
2026-04-20 16:52:20 +02:00
|
|
|
),
|
2026-05-01 10:11:44 +02:00
|
|
|
status: OperationsStatus.ready,
|
2026-04-20 16:52:20 +02:00
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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,
|
|
|
|
|
}) {
|
2026-05-01 10:11:44 +02:00
|
|
|
if (state.currentOperation == null) return;
|
2026-04-20 16:52:20 +02:00
|
|
|
|
2026-05-01 10:11:44 +02:00
|
|
|
final updated = state.currentOperation!.copyWith(
|
2026-04-20 16:52:20 +02:00
|
|
|
al: al,
|
|
|
|
|
mnp: mnp,
|
|
|
|
|
nip: nip,
|
|
|
|
|
unica: unica,
|
|
|
|
|
telepass: telepass,
|
|
|
|
|
note: note,
|
|
|
|
|
number: number,
|
|
|
|
|
isBozza: isBozza,
|
|
|
|
|
resultOk: resultOk,
|
|
|
|
|
customerId: customerId,
|
|
|
|
|
customerDisplayName: customerDisplayName,
|
|
|
|
|
);
|
|
|
|
|
|
2026-05-01 10:11:44 +02:00
|
|
|
emit(state.copyWith(currentOperation: updated));
|
2026-04-20 16:52:20 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- GESTIONE MODULI COMPLESSI ---
|
|
|
|
|
|
2026-05-01 10:11:44 +02:00
|
|
|
void updateEnergyOperations(List<EnergyOperationModel> energyList) {
|
2026-04-20 16:52:20 +02:00
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
2026-05-01 10:11:44 +02:00
|
|
|
currentOperation: state.currentOperation?.copyWith(
|
|
|
|
|
energyOperations: energyList,
|
2026-04-20 16:52:20 +02:00
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
2026-04-16 11:50:29 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-01 10:11:44 +02:00
|
|
|
void updateFinOperations(List<FinOperationModel> finList) {
|
2026-04-20 16:52:20 +02:00
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
2026-05-01 10:11:44 +02:00
|
|
|
currentOperation: state.currentOperation?.copyWith(
|
|
|
|
|
finOperations: finList,
|
|
|
|
|
),
|
2026-04-20 16:52:20 +02:00
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-01 10:11:44 +02:00
|
|
|
void updateEntertainmentOperations(
|
|
|
|
|
List<EntertainmentOperationModel> entList,
|
|
|
|
|
) {
|
2026-04-20 16:52:20 +02:00
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
2026-05-01 10:11:44 +02:00
|
|
|
currentOperation: state.currentOperation?.copyWith(
|
|
|
|
|
entertainmentOperations: entList,
|
2026-04-20 16:52:20 +02:00
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- PERSISTENZA ---
|
|
|
|
|
|
2026-05-01 10:11:44 +02:00
|
|
|
Future<void> saveCurrentOperation({
|
2026-04-26 10:15:34 +02:00
|
|
|
required bool isBozza,
|
|
|
|
|
bool shouldPop = true,
|
2026-05-01 10:11:44 +02:00
|
|
|
List<OperationFileModel>? files,
|
2026-04-26 10:15:34 +02:00
|
|
|
}) async {
|
2026-05-01 10:11:44 +02:00
|
|
|
if (state.currentOperation == null) return;
|
2026-04-20 16:52:20 +02:00
|
|
|
|
2026-05-01 10:11:44 +02:00
|
|
|
emit(state.copyWith(status: OperationsStatus.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
|
2026-05-01 10:11:44 +02:00
|
|
|
final operationToSave = state.currentOperation!.copyWith(
|
2026-04-26 10:15:34 +02:00
|
|
|
isBozza: isBozza,
|
|
|
|
|
files: files,
|
|
|
|
|
);
|
2026-04-20 16:52:20 +02:00
|
|
|
|
|
|
|
|
// 2. Salvataggio corazzato
|
2026-05-01 10:11:44 +02:00
|
|
|
final updatedOperation = await _repository.saveFullOperation(
|
|
|
|
|
operationToSave,
|
|
|
|
|
);
|
2026-04-20 16:52:20 +02:00
|
|
|
|
|
|
|
|
// 3. Reset e ricaricamento
|
2026-04-26 10:15:34 +02:00
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
2026-05-01 10:11:44 +02:00
|
|
|
status: shouldPop
|
|
|
|
|
? OperationsStatus.saved
|
|
|
|
|
: OperationsStatus.savedNoPop,
|
|
|
|
|
currentOperation: shouldPop ? null : updatedOperation,
|
2026-04-26 10:15:34 +02:00
|
|
|
),
|
|
|
|
|
);
|
2026-05-01 10:11:44 +02:00
|
|
|
await loadOperations(refresh: true);
|
2026-04-16 11:50:29 +02:00
|
|
|
} catch (e) {
|
2026-04-20 16:52:20 +02:00
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
2026-05-01 10:11:44 +02:00
|
|
|
status: OperationsStatus.failure,
|
2026-04-20 16:52:20 +02:00
|
|
|
errorMessage: e.toString(),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- GESTIONE ALLEGATI LOCALI ---
|
|
|
|
|
|
|
|
|
|
void addAttachments(List<PlatformFile> files) {
|
|
|
|
|
final newAttachments = files.map((file) {
|
2026-05-01 10:11:44 +02:00
|
|
|
return OperationFileModel(
|
2026-04-20 16:52:20 +02:00
|
|
|
id: null, // Meglio null se non è su DB
|
2026-05-01 10:11:44 +02:00
|
|
|
operationId: state.currentOperation?.id ?? '',
|
2026-04-20 16:52:20 +02:00
|
|
|
name: file.name.fileNameWithoutExtension(),
|
|
|
|
|
extension: file.name.fileExtension(),
|
2026-04-26 10:15:34 +02:00
|
|
|
storagePath: '',
|
2026-04-20 16:52:20 +02:00
|
|
|
fileSize: file.size,
|
|
|
|
|
localBytes: file.bytes,
|
|
|
|
|
createdAt: DateTime.now(),
|
|
|
|
|
);
|
|
|
|
|
}).toList();
|
|
|
|
|
|
|
|
|
|
// Creiamo una nuova lista pulita
|
2026-05-01 10:11:44 +02:00
|
|
|
final List<OperationFileModel> updatedList = [
|
|
|
|
|
...(state.currentOperation?.files ?? []),
|
2026-04-20 16:52:20 +02:00
|
|
|
...newAttachments,
|
|
|
|
|
];
|
|
|
|
|
|
2026-05-01 10:11:44 +02:00
|
|
|
// Emettiamo lo stato assicurandoci che il OperationModel venga clonato
|
|
|
|
|
if (state.currentOperation != null) {
|
2026-04-20 16:52:20 +02:00
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
2026-05-01 10:11:44 +02:00
|
|
|
currentOperation: state.currentOperation!.copyWith(
|
|
|
|
|
files: updatedList,
|
|
|
|
|
),
|
2026-04-20 16:52:20 +02:00
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void removeAttachment(int index) {
|
2026-05-01 10:11:44 +02:00
|
|
|
if (state.currentOperation == null) return;
|
2026-04-20 16:52:20 +02:00
|
|
|
|
2026-05-01 10:11:44 +02:00
|
|
|
final updatedList = List<OperationFileModel>.from(
|
|
|
|
|
state.currentOperation!.files,
|
2026-04-20 16:52:20 +02:00
|
|
|
);
|
|
|
|
|
updatedList.removeAt(index);
|
|
|
|
|
|
|
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
2026-05-01 10:11:44 +02:00
|
|
|
currentOperation: state.currentOperation?.copyWith(files: updatedList),
|
2026-04-20 16:52:20 +02:00
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-01 10:11:44 +02:00
|
|
|
void saveAndCopyFileToCustomer(List<OperationFileModel> selectedFiles) async {
|
|
|
|
|
final currentOperation = state.currentOperation;
|
2026-04-26 10:15:34 +02:00
|
|
|
|
|
|
|
|
// 1. Check di sicurezza: se non c'è il cliente, non sappiamo dove copiare
|
2026-05-01 10:11:44 +02:00
|
|
|
if (currentOperation == null || currentOperation.customerId == null) {
|
2026-04-26 10:15:34 +02:00
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
2026-05-01 10:11:44 +02:00
|
|
|
status: OperationsStatus.failure,
|
2026-04-26 10:15:34 +02:00
|
|
|
errorMessage:
|
|
|
|
|
"Impossibile copiare: nessun cliente associato alla pratica.",
|
|
|
|
|
),
|
|
|
|
|
);
|
2026-04-20 16:52:20 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-01 10:11:44 +02:00
|
|
|
emit(state.copyWith(status: OperationsStatus.loading));
|
2026-04-20 16:52:20 +02:00
|
|
|
|
|
|
|
|
try {
|
2026-04-26 10:15:34 +02:00
|
|
|
// 2. SALVATAGGIO CORAZZATO
|
|
|
|
|
// Chiamiamo il repo e otteniamo la pratica con TUTTI i file ora dotati di ID e storagePath
|
2026-05-01 10:11:44 +02:00
|
|
|
final updatedOperation = await _repository.saveFullOperation(
|
|
|
|
|
currentOperation,
|
|
|
|
|
);
|
2026-04-26 10:15:34 +02:00
|
|
|
|
|
|
|
|
// 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
|
2026-05-01 10:11:44 +02:00
|
|
|
final persistedFile = updatedOperation.files.firstWhere(
|
2026-04-26 10:15:34 +02:00
|
|
|
(f) =>
|
|
|
|
|
f.name == selectedFile.name &&
|
|
|
|
|
f.extension == selectedFile.extension,
|
|
|
|
|
orElse: () => throw Exception(
|
|
|
|
|
"File ${selectedFile.name} non trovato dopo il salvataggio.",
|
|
|
|
|
),
|
|
|
|
|
);
|
2026-04-20 16:52:20 +02:00
|
|
|
|
2026-04-26 10:15:34 +02:00
|
|
|
// Creiamo il link nel database del cliente
|
|
|
|
|
await _repository.copyFileToCustomer(
|
|
|
|
|
file: persistedFile,
|
2026-05-01 10:11:44 +02:00
|
|
|
customerId: currentOperation.customerId!,
|
2026-04-20 16:52:20 +02:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-26 10:15:34 +02:00
|
|
|
// 4. AGGIORNAMENTO STATO
|
|
|
|
|
// Aggiorniamo il Cubit con il servizio salvato così la UI mostra i file come "Remoti"
|
|
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
2026-05-01 10:11:44 +02:00
|
|
|
status: OperationsStatus.success,
|
|
|
|
|
currentOperation: updatedOperation,
|
2026-04-26 10:15:34 +02:00
|
|
|
),
|
2026-04-20 16:52:20 +02:00
|
|
|
);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
2026-05-01 10:11:44 +02:00
|
|
|
status: OperationsStatus.failure,
|
2026-04-26 10:15:34 +02:00
|
|
|
errorMessage: "Errore durante il salvataggio e copia: $e",
|
2026-04-20 16:52:20 +02:00
|
|
|
),
|
|
|
|
|
);
|
2026-04-16 11:50:29 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|