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 12:41:07 +02:00
|
|
|
final StaffMemberModel? _createdBy;
|
2026-05-07 16:28:01 +02:00
|
|
|
|
2026-05-13 12:41:07 +02:00
|
|
|
TicketFormCubit({StaffMemberModel? createdBy})
|
2026-05-07 16:28:01 +02:00
|
|
|
: super(
|
|
|
|
|
// Inizializziamo con un ticket vuoto di default
|
2026-05-13 12:41:07 +02:00
|
|
|
TicketFormState(
|
|
|
|
|
ticket: TicketModel.empty().copyWith(
|
|
|
|
|
createdById: createdBy?.id,
|
|
|
|
|
createdByName: createdBy?.name,
|
|
|
|
|
),
|
|
|
|
|
),
|
2026-05-07 16:28:01 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
/// 1. INIZIALIZZAZIONE (Se stiamo modificando un ticket esistente)
|
|
|
|
|
Future<void> initForm({String? id, TicketModel? existingTicket}) async {
|
|
|
|
|
if (existingTicket != null) {
|
|
|
|
|
// SCENARIO 1 (App Native / Navigazione interna Web):
|
|
|
|
|
// Abbiamo l'oggetto intero passato via 'extra'. Lo mostriamo all'istante!
|
|
|
|
|
emit(
|
|
|
|
|
state.copyWith(ticket: existingTicket, status: TicketFormStatus.ready),
|
|
|
|
|
);
|
|
|
|
|
} else if (id != null) {
|
|
|
|
|
// SCENARIO 2 (Web Refresh o Link condiviso):
|
|
|
|
|
// L'utente ha premuto F5 su /tickets/form/123. L'extra è andato perso, ma abbiamo l'ID!
|
|
|
|
|
emit(
|
|
|
|
|
state.copyWith(status: TicketFormStatus.loading),
|
|
|
|
|
); // Mostriamo uno spinner
|
|
|
|
|
try {
|
|
|
|
|
final fetchedTicket = await _repository.getTicketById(
|
|
|
|
|
id,
|
|
|
|
|
); // Lo scarichiamo!
|
|
|
|
|
emit(
|
|
|
|
|
state.copyWith(ticket: fetchedTicket, status: TicketFormStatus.ready),
|
|
|
|
|
);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
|
|
|
|
status: TicketFormStatus.failure,
|
|
|
|
|
errorMessage: 'Ticket non trovato',
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// SCENARIO 3 (Nuovo Ticket):
|
|
|
|
|
// È un nuovo ticket! Inseriamo i default base (Azienda, Negozio, Creatore)
|
|
|
|
|
final currentStore = _sessionCubit.state.currentStore;
|
|
|
|
|
final companyId = _sessionCubit.state.company?.id ?? '';
|
|
|
|
|
|
|
|
|
|
final newTicket = TicketModel.empty().copyWith(
|
|
|
|
|
companyId: companyId,
|
|
|
|
|
storeId: currentStore?.id,
|
2026-05-13 12:41:07 +02:00
|
|
|
createdById: createdBy.id,
|
|
|
|
|
createdByName: _createdBy.name,
|
2026-05-07 16:28:01 +02:00
|
|
|
// Impostiamo lo stato iniziale
|
|
|
|
|
ticketStatus: TicketStatus.open,
|
|
|
|
|
ticketType: TicketType.repair, // Default
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
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-12 11:14:48 +02:00
|
|
|
String? sourceSn,
|
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,
|
|
|
|
|
}) {
|
|
|
|
|
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-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,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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-07 16:28:01 +02:00
|
|
|
}
|