2026-04-16 11:50:29 +02:00
|
|
|
import 'package:equatable/equatable.dart';
|
|
|
|
|
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-05-01 10:11:44 +02:00
|
|
|
import 'package:flux/features/operations/data/operations_repository.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-02 10:22:47 +02:00
|
|
|
import 'package:uuid/uuid.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-05-02 10:22:47 +02:00
|
|
|
final Uuid _uuid = const Uuid(); // Generatore di UUID per il batch
|
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 {
|
|
|
|
|
if (state.status == OperationsStatus.loading) return;
|
2026-04-20 16:52:20 +02:00
|
|
|
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,
|
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-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-05-02 10:22:47 +02:00
|
|
|
errorMessage: "Errore nel caricamento operazioni: $e",
|
2026-04-20 16:52:20 +02:00
|
|
|
),
|
|
|
|
|
);
|
2026-04-16 11:50:29 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 16:52:20 +02:00
|
|
|
// --- GESTIONE FILTRI ---
|
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
|
2026-05-01 10:11:44 +02:00
|
|
|
void initOperationForm({
|
|
|
|
|
OperationModel? existingOperation,
|
|
|
|
|
String? operationId,
|
2026-05-03 12:44:51 +02:00
|
|
|
String? staffId,
|
|
|
|
|
String? staffDisplayName,
|
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 {
|
2026-05-02 10:22:47 +02:00
|
|
|
// NUOVA PRATICA: Creiamo un nuovo Batch UUID
|
2026-04-20 16:52:20 +02:00
|
|
|
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-05-02 10:22:47 +02:00
|
|
|
reference: '',
|
2026-04-20 16:52:20 +02:00
|
|
|
createdAt: DateTime.now(),
|
2026-04-22 11:06:02 +02:00
|
|
|
companyId: _sessionCubit.state.company!.id!,
|
2026-05-02 10:22:47 +02:00
|
|
|
status: OperationStatus.draft,
|
|
|
|
|
batchUuid: _uuid.v4(), // <-- GENERIAMO IL BATCH UNIVOCO
|
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
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-02 10:22:47 +02:00
|
|
|
/// MAGIA PURA: Prepara il form per inserire un altro servizio nella stessa pratica.
|
|
|
|
|
/// Mantiene il Cliente, il Batch e lo Store, ma svuota il resto.
|
|
|
|
|
void prepareNextOperationInBatch() {
|
2026-05-01 10:11:44 +02:00
|
|
|
if (state.currentOperation == null) return;
|
2026-04-20 16:52:20 +02:00
|
|
|
|
2026-05-02 10:22:47 +02:00
|
|
|
final current = state.currentOperation!;
|
2026-04-20 16:52:20 +02:00
|
|
|
|
|
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
2026-05-02 10:22:47 +02:00
|
|
|
status: OperationsStatus.ready,
|
|
|
|
|
currentOperation: OperationModel(
|
|
|
|
|
companyId: current.companyId,
|
|
|
|
|
storeId: current.storeId,
|
|
|
|
|
storeDisplayName: current.storeDisplayName,
|
|
|
|
|
batchUuid: current.batchUuid, // <-- MANTIENE IL COLLEGAMENTO
|
|
|
|
|
customerId: current.customerId, // <-- MANTIENE IL CLIENTE
|
|
|
|
|
customerDisplayName: current.customerDisplayName,
|
|
|
|
|
status: OperationStatus.draft,
|
|
|
|
|
createdAt: DateTime.now(),
|
2026-04-20 16:52:20 +02:00
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- PERSISTENZA ---
|
|
|
|
|
|
2026-05-01 10:11:44 +02:00
|
|
|
Future<void> saveCurrentOperation({
|
2026-05-02 10:22:47 +02:00
|
|
|
required OperationStatus targetStatus,
|
2026-04-26 10:15:34 +02:00
|
|
|
bool shouldPop = true,
|
|
|
|
|
}) 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-05-01 10:11:44 +02:00
|
|
|
final operationToSave = state.currentOperation!.copyWith(
|
2026-05-02 10:22:47 +02:00
|
|
|
status: targetStatus,
|
2026-04-26 10:15:34 +02:00
|
|
|
);
|
2026-04-20 16:52:20 +02:00
|
|
|
|
2026-05-01 10:11:44 +02:00
|
|
|
final updatedOperation = await _repository.saveFullOperation(
|
2026-05-03 12:05:47 +02:00
|
|
|
operation: operationToSave,
|
2026-05-01 10:11:44 +02:00
|
|
|
);
|
2026-04-20 16:52:20 +02:00
|
|
|
|
2026-04-26 10:15:34 +02:00
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
2026-05-02 10:22:47 +02:00
|
|
|
// Se non facciamo Pop (es. l'utente vuole aggiungere un altro servizio), non killiamo l'operazione corrente
|
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-02 10:22:47 +02:00
|
|
|
|
|
|
|
|
// Ricarica in background per la dashboard
|
|
|
|
|
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(),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-02 10:22:47 +02:00
|
|
|
// --- RECUPERO OPERAZIONI DELLO STESSO BATCH (Per UI di riepilogo) ---
|
|
|
|
|
|
|
|
|
|
/// Puoi usare questa funzione se nella UI vuoi mostrare "Hai inserito 3 servizi in questa pratica"
|
|
|
|
|
List<OperationModel> getOperationsInCurrentBatch() {
|
|
|
|
|
if (state.currentOperation == null) return [];
|
|
|
|
|
final currentBatch = state.currentOperation!.batchUuid;
|
|
|
|
|
|
|
|
|
|
// Filtriamo dalla lista caricata (o potresti fare una query diretta a Supabase se preferisci)
|
|
|
|
|
return state.allOperations
|
|
|
|
|
.where(
|
|
|
|
|
(op) =>
|
|
|
|
|
op.batchUuid == currentBatch &&
|
|
|
|
|
op.id != state.currentOperation!.id,
|
|
|
|
|
)
|
|
|
|
|
.toList();
|
2026-04-20 16:52:20 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-02 12:19:04 +02:00
|
|
|
// --- GESTIONE DELLO STATO DEL FORM IN TEMPO REALE ---
|
|
|
|
|
void updateOperationFields({
|
|
|
|
|
String? customerId,
|
|
|
|
|
String? customerDisplayName,
|
|
|
|
|
String? type,
|
|
|
|
|
String? providerId,
|
2026-05-03 10:08:57 +02:00
|
|
|
String? providerDisplayName,
|
2026-05-02 12:19:04 +02:00
|
|
|
String? subtype,
|
|
|
|
|
DateTime? expirationDate,
|
|
|
|
|
int? quantity,
|
2026-05-03 10:08:57 +02:00
|
|
|
String? modelId,
|
|
|
|
|
String? modelDisplayName,
|
2026-05-03 12:44:51 +02:00
|
|
|
String? staffId,
|
|
|
|
|
String? staffDisplayName,
|
2026-05-02 12:19:04 +02:00
|
|
|
// Aggiungiamo questi flag per forzare la pulizia dei campi quando cambi tipo
|
|
|
|
|
bool clearProvider = false,
|
|
|
|
|
bool clearType = false,
|
|
|
|
|
bool clearSubtype = false,
|
|
|
|
|
bool clearExpiration = false,
|
2026-05-03 10:08:57 +02:00
|
|
|
bool clearQuantity = false,
|
|
|
|
|
bool clearModel = false,
|
2026-05-02 12:19:04 +02:00
|
|
|
}) {
|
2026-05-01 10:11:44 +02:00
|
|
|
if (state.currentOperation == null) return;
|
2026-05-02 12:19:04 +02:00
|
|
|
|
|
|
|
|
final current = state.currentOperation!;
|
|
|
|
|
|
|
|
|
|
// Creiamo il modello aggiornato
|
|
|
|
|
// ATTENZIONE: adatta questa logica in base a come è scritto il tuo copyWith!
|
2026-05-03 12:05:47 +02:00
|
|
|
int? newQuantity;
|
|
|
|
|
if (clearQuantity) {
|
|
|
|
|
newQuantity = 1;
|
|
|
|
|
}
|
|
|
|
|
if (quantity != null && quantity <= 0) {
|
|
|
|
|
newQuantity = 0;
|
|
|
|
|
}
|
|
|
|
|
if (quantity != null && quantity > 0) {
|
|
|
|
|
newQuantity = quantity;
|
|
|
|
|
}
|
2026-05-02 12:19:04 +02:00
|
|
|
final updated = current.copyWith(
|
2026-05-02 10:22:47 +02:00
|
|
|
customerId: customerId,
|
|
|
|
|
customerDisplayName: customerDisplayName,
|
2026-05-03 10:08:57 +02:00
|
|
|
|
2026-05-02 12:19:04 +02:00
|
|
|
// Se clearProvider è true, forziamo una stringa vuota (o null se il tuo modello lo supporta)
|
|
|
|
|
providerId: clearProvider ? null : (providerId ?? current.providerId),
|
2026-05-03 10:08:57 +02:00
|
|
|
providerDisplayName: clearProvider
|
|
|
|
|
? null
|
|
|
|
|
: (providerDisplayName ?? current.providerDisplayName),
|
2026-05-03 12:05:47 +02:00
|
|
|
quantity: newQuantity,
|
2026-05-03 10:08:57 +02:00
|
|
|
type: clearType ? null : (type ?? current.type),
|
|
|
|
|
subtype: clearSubtype ? null : (subtype ?? current.subtype),
|
|
|
|
|
expirationDate: clearExpiration
|
|
|
|
|
? null
|
|
|
|
|
: (expirationDate ?? current.expirationDate),
|
|
|
|
|
modelId: clearModel ? null : (modelId ?? current.modelId),
|
|
|
|
|
modelDisplayName: clearModel
|
|
|
|
|
? null
|
|
|
|
|
: (modelDisplayName ?? current.modelDisplayName),
|
2026-05-03 12:44:51 +02:00
|
|
|
staffId: staffId ?? current.staffId,
|
|
|
|
|
staffDisplayName: staffDisplayName ?? current.staffDisplayName,
|
2026-04-20 16:52:20 +02:00
|
|
|
);
|
2026-05-02 12:19:04 +02:00
|
|
|
|
2026-05-02 10:22:47 +02:00
|
|
|
emit(state.copyWith(currentOperation: updated));
|
2026-04-16 11:50:29 +02:00
|
|
|
}
|
2026-05-03 10:08:57 +02:00
|
|
|
|
|
|
|
|
// Metodo di utilità per calcolare la data X mesi da oggi
|
|
|
|
|
DateTime _calculateMonths(int months) {
|
|
|
|
|
final now = DateTime.now();
|
|
|
|
|
return DateTime(now.year, now.month + months, now.day);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Quando l'utente seleziona un tipo, impostiamo il default
|
|
|
|
|
void setTypeWithSmartDefault(String type) {
|
|
|
|
|
DateTime? defaultDate;
|
|
|
|
|
|
|
|
|
|
if (type == 'Energy') defaultDate = _calculateMonths(24);
|
|
|
|
|
if (type == 'Fin') defaultDate = _calculateMonths(30);
|
|
|
|
|
if (type == 'Entertainment') defaultDate = _calculateMonths(12);
|
|
|
|
|
|
|
|
|
|
updateOperationFields(
|
|
|
|
|
type: type,
|
|
|
|
|
expirationDate: defaultDate,
|
|
|
|
|
clearProvider: true,
|
|
|
|
|
clearSubtype: true,
|
|
|
|
|
clearModel: true,
|
|
|
|
|
clearQuantity: true,
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-04-16 11:50:29 +02:00
|
|
|
}
|