2026-05-07 16:28:01 +02:00
|
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
|
|
|
import 'package:flux/core/blocs/session/session_cubit.dart';
|
|
|
|
|
import 'package:flux/features/customers/models/customer_model.dart';
|
2026-05-13 12:41:07 +02:00
|
|
|
import 'package:flux/features/master_data/staff/models/staff_member_model.dart';
|
2026-05-07 16:28:01 +02:00
|
|
|
import 'package:flux/features/tickets/models/ticket_model.dart';
|
|
|
|
|
import 'package:flux/features/tickets/data/ticket_repository.dart';
|
2026-05-13 12:41:07 +02:00
|
|
|
import 'package:flux/features/tracking/data/tracking_repository.dart';
|
|
|
|
|
import 'package:flux/features/tracking/models/tracking_model.dart';
|
2026-05-07 16:28:01 +02:00
|
|
|
import 'package:get_it/get_it.dart';
|
|
|
|
|
import 'ticket_form_state.dart';
|
|
|
|
|
|
|
|
|
|
class TicketFormCubit extends Cubit<TicketFormState> {
|
|
|
|
|
final TicketRepository _repository = GetIt.I.get<TicketRepository>();
|
|
|
|
|
final SessionCubit _sessionCubit = GetIt.I.get<SessionCubit>();
|
|
|
|
|
|
2026-05-13 15:41:35 +02:00
|
|
|
// Costruttore: prepariamo subito il ticket base con i dati di chi lo crea
|
|
|
|
|
TicketFormCubit({StaffMemberModel? createdBy, TicketModel? existingTicket})
|
2026-05-07 16:28:01 +02:00
|
|
|
: super(
|
2026-05-13 12:41:07 +02:00
|
|
|
TicketFormState(
|
2026-05-13 15:41:35 +02:00
|
|
|
// Se c'è un ticket esistente usa quello, ALTRIMENTI ne crea uno vuoto
|
|
|
|
|
// e ci stampa subito il nome del creatore!
|
|
|
|
|
ticket:
|
|
|
|
|
existingTicket ??
|
|
|
|
|
TicketModel.empty().copyWith(
|
|
|
|
|
createdById: createdBy?.id,
|
|
|
|
|
createdByName: createdBy?.name,
|
|
|
|
|
),
|
2026-05-13 12:41:07 +02:00
|
|
|
),
|
2026-05-07 16:28:01 +02:00
|
|
|
);
|
|
|
|
|
|
2026-05-13 15:41:35 +02:00
|
|
|
/// 1. INIZIALIZZAZIONE
|
2026-05-07 16:28:01 +02:00
|
|
|
Future<void> initForm({String? id, TicketModel? existingTicket}) async {
|
|
|
|
|
if (existingTicket != null) {
|
2026-05-13 15:41:35 +02:00
|
|
|
// SCENARIO 1: Abbiamo il ticket intero passato via record
|
2026-05-07 16:28:01 +02:00
|
|
|
emit(
|
|
|
|
|
state.copyWith(ticket: existingTicket, status: TicketFormStatus.ready),
|
|
|
|
|
);
|
|
|
|
|
} else if (id != null) {
|
2026-05-13 15:41:35 +02:00
|
|
|
// SCENARIO 2: QR CODE o Web Refresh! (Hai solo l'ID)
|
|
|
|
|
emit(state.copyWith(status: TicketFormStatus.loading));
|
|
|
|
|
|
2026-05-07 16:28:01 +02:00
|
|
|
try {
|
2026-05-13 15:41:35 +02:00
|
|
|
// Boom! Lo scarica dal database in tempo reale
|
|
|
|
|
final fetchedTicket = await _repository.getTicketById(id);
|
2026-05-07 16:28:01 +02:00
|
|
|
emit(
|
|
|
|
|
state.copyWith(ticket: fetchedTicket, status: TicketFormStatus.ready),
|
|
|
|
|
);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
|
|
|
|
status: TicketFormStatus.failure,
|
|
|
|
|
errorMessage: 'Ticket non trovato',
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2026-05-13 15:41:35 +02:00
|
|
|
// SCENARIO 3: Nuovo Ticket
|
2026-05-07 16:28:01 +02:00
|
|
|
final currentStore = _sessionCubit.state.currentStore;
|
|
|
|
|
final companyId = _sessionCubit.state.company?.id ?? '';
|
|
|
|
|
|
2026-05-13 15:41:35 +02:00
|
|
|
// IL TRUCCO È QUI: Usiamo `state.ticket` invece di `TicketModel.empty()`.
|
|
|
|
|
// `state.ticket` HA GIÀ i dati di 'createdBy' settati nel costruttore!
|
|
|
|
|
final newTicket = state.ticket.copyWith(
|
2026-05-07 16:28:01 +02:00
|
|
|
companyId: companyId,
|
|
|
|
|
storeId: currentStore?.id,
|
2026-05-13 15:41:35 +02:00
|
|
|
ticketStatus: TicketStatus.open, // <-- O il tuo status di default
|
|
|
|
|
ticketType: TicketType.repair,
|
2026-05-07 16:28:01 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
emit(state.copyWith(ticket: newTicket, status: TicketFormStatus.ready));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 2. AGGIORNAMENTO CLIENTE (Usato dal nostro SharedCustomerSection!)
|
|
|
|
|
void updateCustomer(CustomerModel customer) {
|
|
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
|
|
|
|
ticket: state.ticket.copyWith(
|
|
|
|
|
customerId: customer.id,
|
2026-05-12 11:14:48 +02:00
|
|
|
customer: customer,
|
|
|
|
|
alternativePhoneNumber:
|
|
|
|
|
state.ticket.alternativePhoneNumber ?? customer.phoneNumber,
|
2026-05-07 16:28:01 +02:00
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 3. AGGIORNAMENTO MODELLO (Usato dal nostro SharedModelSection!)
|
2026-05-11 11:44:14 +02:00
|
|
|
void updateTargetModel({required String modelId, required String modelName}) {
|
2026-05-07 16:28:01 +02:00
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
|
|
|
|
ticket: state.ticket.copyWith(
|
|
|
|
|
targetModelId: modelId,
|
|
|
|
|
targetModelName: modelName,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-12 11:14:48 +02:00
|
|
|
void updateSourceModel({required String modelId, required String modelName}) {
|
|
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
|
|
|
|
ticket: state.ticket.copyWith(
|
|
|
|
|
sourceModelId: modelId,
|
|
|
|
|
sourceModelName: modelName,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-07 16:28:01 +02:00
|
|
|
void updateCreator({required String staffId, required String staffName}) {
|
|
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
|
|
|
|
ticket: state.ticket.copyWith(
|
|
|
|
|
createdById: staffId,
|
|
|
|
|
createdByName: staffName,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 4. AGGIORNAMENTO GENERICO DEI CAMPI
|
|
|
|
|
void updateFields({
|
|
|
|
|
TicketType? ticketType,
|
|
|
|
|
TicketStatus? status,
|
|
|
|
|
String? request,
|
|
|
|
|
String? targetSn,
|
2026-05-14 12:07:05 +02:00
|
|
|
String? targetPassword,
|
2026-05-12 11:14:48 +02:00
|
|
|
String? sourceSn,
|
2026-05-14 12:07:05 +02:00
|
|
|
String? sourcePassword,
|
2026-05-07 16:28:01 +02:00
|
|
|
String? alternativePhoneNumber,
|
|
|
|
|
bool? hasCourtesyDevice,
|
|
|
|
|
String? includedAccessories,
|
|
|
|
|
String? publicNotes,
|
|
|
|
|
String? internalNotes,
|
|
|
|
|
double? customerPrice,
|
|
|
|
|
double? internalCost,
|
|
|
|
|
String? assignedToId,
|
|
|
|
|
String? assignedToName,
|
2026-05-14 12:07:05 +02:00
|
|
|
WarrantyType? warrantyType,
|
2026-05-07 16:28:01 +02:00
|
|
|
}) {
|
|
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
|
|
|
|
ticket: state.ticket.copyWith(
|
|
|
|
|
ticketType: ticketType ?? state.ticket.ticketType,
|
|
|
|
|
ticketStatus: status ?? state.ticket.ticketStatus,
|
|
|
|
|
request: request ?? state.ticket.request,
|
|
|
|
|
targetSn: targetSn ?? state.ticket.targetSn,
|
2026-05-14 12:07:05 +02:00
|
|
|
targetPassword: targetPassword ?? state.ticket.targetPassword,
|
|
|
|
|
sourcePassword: sourcePassword ?? state.ticket.sourcePassword,
|
2026-05-12 11:14:48 +02:00
|
|
|
sourceSn: sourceSn ?? state.ticket.sourceSn,
|
2026-05-07 16:28:01 +02:00
|
|
|
alternativePhoneNumber:
|
|
|
|
|
alternativePhoneNumber ?? state.ticket.alternativePhoneNumber,
|
|
|
|
|
hasCourtesyDevice:
|
|
|
|
|
hasCourtesyDevice ?? state.ticket.hasCourtesyDevice,
|
|
|
|
|
includedAccessories:
|
|
|
|
|
includedAccessories ?? state.ticket.includedAccessories,
|
|
|
|
|
publicNotes: publicNotes ?? state.ticket.publicNotes,
|
|
|
|
|
internalNotes: internalNotes ?? state.ticket.internalNotes,
|
|
|
|
|
customerPrice: customerPrice ?? state.ticket.customerPrice,
|
|
|
|
|
internalCost: internalCost ?? state.ticket.internalCost,
|
|
|
|
|
assignedToId: assignedToId ?? state.ticket.assignedToId,
|
|
|
|
|
assignedToName: assignedToName ?? state.ticket.assignedToName,
|
2026-05-14 12:07:05 +02:00
|
|
|
warrantyType: warrantyType ?? state.ticket.warrantyType,
|
2026-05-07 16:28:01 +02:00
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 5. SALVATAGGIO
|
2026-05-11 11:44:14 +02:00
|
|
|
Future<void> saveTicket() async {
|
2026-05-07 16:28:01 +02:00
|
|
|
emit(state.copyWith(status: TicketFormStatus.saving));
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
final ticketToSave = state.ticket;
|
|
|
|
|
|
|
|
|
|
// Validazione base
|
|
|
|
|
if (ticketToSave.customerId == null || ticketToSave.customerId!.isEmpty) {
|
|
|
|
|
throw Exception("Seleziona un cliente prima di salvare.");
|
|
|
|
|
}
|
2026-05-10 14:09:57 +02:00
|
|
|
TicketModel? savedTicket;
|
|
|
|
|
if (ticketToSave.id == null) {
|
|
|
|
|
savedTicket = await _repository.insertTicket(ticketToSave);
|
|
|
|
|
} else {
|
|
|
|
|
savedTicket = await _repository.updateTicket(ticketToSave);
|
|
|
|
|
}
|
2026-05-11 11:44:14 +02:00
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
|
|
|
|
status: TicketFormStatus.success,
|
|
|
|
|
ticket: ticketToSave.copyWith(
|
|
|
|
|
id: savedTicket.id,
|
|
|
|
|
referenceId: savedTicket.referenceId,
|
2026-05-07 16:28:01 +02:00
|
|
|
),
|
2026-05-11 11:44:14 +02:00
|
|
|
),
|
|
|
|
|
);
|
2026-05-07 16:28:01 +02:00
|
|
|
} catch (e) {
|
|
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
|
|
|
|
status: TicketFormStatus.failure,
|
|
|
|
|
errorMessage: e.toString(),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 5.1 SALVATAGGIO SILENZIOSO (Per generare il QR Code al volo)
|
|
|
|
|
Future<String?> saveTicketDraft() async {
|
|
|
|
|
// Non mettiamo lo stato 'saving' per non far sfarfallare tutta la UI,
|
|
|
|
|
// usiamo un caricamento invisibile.
|
|
|
|
|
try {
|
|
|
|
|
final ticketToSave = state.ticket;
|
|
|
|
|
|
|
|
|
|
if (ticketToSave.customerId == null || ticketToSave.customerId!.isEmpty) {
|
|
|
|
|
throw Exception("Seleziona un cliente prima di poter usare il QR.");
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 14:09:57 +02:00
|
|
|
final savedTicket = await _repository.insertTicket(ticketToSave);
|
2026-05-07 16:28:01 +02:00
|
|
|
|
|
|
|
|
// Aggiorniamo silenziosamente lo stato con il ticket che ora ha un ID!
|
|
|
|
|
emit(state.copyWith(ticket: savedTicket, status: TicketFormStatus.ready));
|
|
|
|
|
|
|
|
|
|
return savedTicket.id;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
|
|
|
|
status: TicketFormStatus.failure,
|
|
|
|
|
errorMessage: e.toString(),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-13 12:41:07 +02:00
|
|
|
|
|
|
|
|
Future<void> takeInCharge({
|
|
|
|
|
required String staffId,
|
|
|
|
|
required String staffName,
|
|
|
|
|
}) async {
|
|
|
|
|
final currentTicket = state.ticket;
|
|
|
|
|
|
|
|
|
|
// Sicurezza: non possiamo prendere in carico un ticket fantasma
|
|
|
|
|
if (currentTicket.id == null || currentTicket.id!.isEmpty) return;
|
|
|
|
|
|
|
|
|
|
// 1. Prepariamo il ticket aggiornato
|
|
|
|
|
final updatedTicket = currentTicket.copyWith(
|
|
|
|
|
ticketStatus: TicketStatus
|
|
|
|
|
.inProgress, // Assumendo che tu abbia un enum per gli stati
|
|
|
|
|
assignedToId: staffId,
|
|
|
|
|
assignedToName: staffName,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 2. Aggiorniamo il ticket sul Database (usa il tuo metodo esistente del repo)
|
|
|
|
|
await _repository.updateTicket(updatedTicket);
|
|
|
|
|
|
|
|
|
|
// 3. Spara il log automatico nella Timeline!
|
|
|
|
|
await GetIt.I.get<TrackingRepository>().logQuickEvent(
|
|
|
|
|
companyId: currentTicket.companyId,
|
|
|
|
|
message: "Ticket preso in carico. Inizio lavorazione.",
|
|
|
|
|
type: TrackingType.statusChange,
|
|
|
|
|
parentId: currentTicket.id!,
|
|
|
|
|
parentType: TrackingParentType.ticket,
|
|
|
|
|
staffId: staffId,
|
|
|
|
|
// Lo mettiamo pubblico (isInternal: false) così il cliente a casa vede che
|
|
|
|
|
// il suo dispositivo è ufficialmente sotto i ferri!
|
|
|
|
|
isInternal: false,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 4. Aggiorniamo lo stato locale del Cubit per far scattare la UI
|
|
|
|
|
emit(state.copyWith(ticket: updatedTicket));
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// Gestisci eventuali errori (es. mostrando una snackbar)
|
|
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
|
|
|
|
status: TicketFormStatus.failure,
|
|
|
|
|
errorMessage: 'Errore durante la presa in carico: $e',
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-14 12:07:05 +02:00
|
|
|
|
|
|
|
|
Future<void> pauseTicket({
|
|
|
|
|
required TicketStatus newStatus,
|
|
|
|
|
required String notes,
|
|
|
|
|
}) async {
|
|
|
|
|
final currentTicket = state.ticket;
|
|
|
|
|
if (currentTicket.id == null || currentTicket.id!.isEmpty) return;
|
|
|
|
|
|
|
|
|
|
// 1. Usiamo lo stato esatto scelto dal tecnico
|
|
|
|
|
final updatedTicket = currentTicket.copyWith(ticketStatus: newStatus);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await _repository.updateTicket(updatedTicket);
|
|
|
|
|
|
|
|
|
|
// 2. Componiamo un bel messaggio per la Timeline
|
|
|
|
|
// Es: "Sospeso per: waitingForQuote. Note: Cliente in ferie"
|
|
|
|
|
String timelineMessage = "Lavorazione sospesa (${newStatus.name}).";
|
|
|
|
|
if (notes.isNotEmpty) {
|
|
|
|
|
timelineMessage += " Note: $notes";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await GetIt.I.get<TrackingRepository>().logQuickEvent(
|
|
|
|
|
companyId: currentTicket.companyId,
|
|
|
|
|
message: timelineMessage,
|
|
|
|
|
type: TrackingType.statusChange,
|
|
|
|
|
parentId: currentTicket.id!,
|
|
|
|
|
parentType: TrackingParentType.ticket,
|
|
|
|
|
staffId: currentTicket.assignedToId ?? '',
|
|
|
|
|
isInternal: false,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
emit(state.copyWith(ticket: updatedTicket));
|
|
|
|
|
} catch (e) {
|
|
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
|
|
|
|
status: TicketFormStatus.failure,
|
|
|
|
|
errorMessage: 'Errore durante la sospensione: $e',
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-14 15:59:46 +02:00
|
|
|
|
|
|
|
|
Future<void> completeTicket({
|
|
|
|
|
required TicketResult result,
|
|
|
|
|
required double finalPrice,
|
|
|
|
|
}) async {
|
|
|
|
|
final currentTicket = state.ticket;
|
|
|
|
|
|
|
|
|
|
if (currentTicket.id == null || currentTicket.id!.isEmpty) return;
|
|
|
|
|
|
|
|
|
|
// 1. Aggiorniamo il ticket con il nuovo status, l'esito e il prezzo!
|
|
|
|
|
final updatedTicket = currentTicket.copyWith(
|
|
|
|
|
ticketStatus: TicketStatus.ready,
|
|
|
|
|
ticketResult: result,
|
|
|
|
|
customerPrice: finalPrice,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await _repository.updateTicket(updatedTicket);
|
|
|
|
|
|
|
|
|
|
// 2. Timeline personalizzata in base all'esito
|
|
|
|
|
final esitoTesto = result == TicketResult.success
|
|
|
|
|
? "Riparato con successo"
|
|
|
|
|
: "Non riparabile/Preventivo rifiutato";
|
|
|
|
|
|
|
|
|
|
await GetIt.I.get<TrackingRepository>().logQuickEvent(
|
|
|
|
|
companyId: currentTicket.companyId,
|
|
|
|
|
message:
|
|
|
|
|
"Lavorazione completata. Esito: $esitoTesto. Il dispositivo è pronto per il ritiro.",
|
|
|
|
|
type: TrackingType.statusChange,
|
|
|
|
|
parentId: currentTicket.id!,
|
|
|
|
|
parentType: TrackingParentType.ticket,
|
|
|
|
|
staffId: currentTicket.assignedToId ?? '',
|
|
|
|
|
isInternal: false,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
emit(state.copyWith(ticket: updatedTicket));
|
|
|
|
|
} catch (e) {
|
|
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
|
|
|
|
status: TicketFormStatus.failure,
|
|
|
|
|
errorMessage: 'Errore durante la chiusura: $e',
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-07 16:28:01 +02:00
|
|
|
}
|