2026-05-07 16:28:01 +02:00
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
import 'package:flux/core/blocs/session/session_cubit.dart';
|
2026-05-16 14:30:23 +02:00
|
|
|
import 'package:flux/features/settings/document_sequence/data/document_sequence_repository.dart';
|
|
|
|
|
import 'package:flux/features/settings/document_sequence/models/document_sequence_model.dart';
|
2026-05-07 16:28:01 +02:00
|
|
|
import 'package:flux/features/tickets/models/ticket_model.dart';
|
|
|
|
|
import 'package:get_it/get_it.dart';
|
|
|
|
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
|
|
|
|
|
|
|
|
|
class TicketRepository {
|
|
|
|
|
final SupabaseClient _supabase = GetIt.I.get<SupabaseClient>();
|
2026-05-16 14:30:23 +02:00
|
|
|
final DocumentSequenceRepository _documentSequenceRepository = GetIt.I
|
|
|
|
|
.get<DocumentSequenceRepository>();
|
2026-05-07 16:28:01 +02:00
|
|
|
|
|
|
|
|
TicketRepository();
|
|
|
|
|
|
|
|
|
|
static const String _tableName = 'ticket';
|
|
|
|
|
|
|
|
|
|
// --- RECUPERO PAGINATO CON FILTRI E JOIN DEI TICKET DI UNO STORE ---
|
|
|
|
|
Future<List<TicketModel>> fetchStoreTickets({
|
|
|
|
|
required int offset,
|
|
|
|
|
int limit = 50,
|
|
|
|
|
String? searchTerm,
|
|
|
|
|
DateTimeRange? dateRange,
|
|
|
|
|
TicketStatus? statusFilter,
|
|
|
|
|
TicketType? ticketTypeFilter,
|
|
|
|
|
String? staffIdFilter,
|
|
|
|
|
}) async {
|
|
|
|
|
try {
|
|
|
|
|
var query = _supabase
|
|
|
|
|
.from(_tableName)
|
|
|
|
|
.select('''
|
|
|
|
|
*,
|
|
|
|
|
customer (*),
|
2026-05-18 12:00:07 +02:00
|
|
|
shipment_document (*, attachments (*)), -- BAM! Deep Join
|
2026-05-07 16:28:01 +02:00
|
|
|
created_by:staff_member!ticket_staff_id_fkey (*),
|
|
|
|
|
assigned_to:staff_member!ticket_assigned_to_id_fkey (*),
|
|
|
|
|
target_model:model!ticket_model_id_1_fkey (*),
|
|
|
|
|
source_model:model!ticket_model_id_2_fkey (*)
|
|
|
|
|
''')
|
|
|
|
|
.eq('store_id', GetIt.I.get<SessionCubit>().state.currentStore!.id!);
|
|
|
|
|
|
|
|
|
|
// Filtro Range Date
|
|
|
|
|
if (dateRange != null) {
|
|
|
|
|
query = query
|
|
|
|
|
.gte('created_at', dateRange.start.toIso8601String())
|
|
|
|
|
.lte('created_at', dateRange.end.toIso8601String());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (statusFilter != null) {
|
|
|
|
|
query = query.eq('status', statusFilter.value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ticketTypeFilter != null) {
|
|
|
|
|
query = query.eq('ticket_type', ticketTypeFilter.value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (staffIdFilter != null) {
|
|
|
|
|
query = query.eq('staff_id', staffIdFilter);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (searchTerm != null && searchTerm.isNotEmpty) {
|
|
|
|
|
// Filtra sui campi della tabella principale O su quelli della tabella joinata
|
|
|
|
|
query = query.or('customer.name.ilike.%$searchTerm%');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final response = await query
|
|
|
|
|
.order('created_at', ascending: false)
|
|
|
|
|
.range(offset, offset + limit - 1);
|
|
|
|
|
|
|
|
|
|
return (response as List).map((map) => TicketModel.fromMap(map)).toList();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
throw Exception('$e');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- RECUPERO PAGINATO CON FILTRI E JOIN DEI TICKET DI TUTTA L'AZIENDA ---
|
|
|
|
|
Future<List<TicketModel>> fetchCompanyTickets({
|
|
|
|
|
required int offset,
|
|
|
|
|
int limit = 50,
|
|
|
|
|
String? searchTerm,
|
|
|
|
|
DateTimeRange? dateRange,
|
|
|
|
|
TicketStatus? ticketStatusFilter,
|
|
|
|
|
TicketType? ticketTypeFilter,
|
|
|
|
|
String? staffIdFilter,
|
|
|
|
|
}) async {
|
|
|
|
|
try {
|
|
|
|
|
var query = _supabase
|
|
|
|
|
.from(_tableName)
|
|
|
|
|
.select('''
|
|
|
|
|
*,
|
|
|
|
|
customer (*),
|
2026-05-18 12:00:07 +02:00
|
|
|
shipment_document (*, attachments (*)),
|
2026-05-07 16:28:01 +02:00
|
|
|
created_by:staff_member!ticket_staff_id_fkey (*),
|
|
|
|
|
assigned_to:staff_member!ticket_assigned_to_id_fkey (*),
|
|
|
|
|
target_model:model!ticket_model_id_1_fkey (*),
|
|
|
|
|
source_model:model!ticket_model_id_2_fkey (*)
|
|
|
|
|
''')
|
|
|
|
|
.eq('company_id', GetIt.I.get<SessionCubit>().state.company!.id!);
|
|
|
|
|
|
|
|
|
|
// Filtro Range Date
|
|
|
|
|
if (dateRange != null) {
|
|
|
|
|
query = query
|
|
|
|
|
.gte('created_at', dateRange.start.toIso8601String())
|
|
|
|
|
.lte('created_at', dateRange.end.toIso8601String());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ticketStatusFilter != null) {
|
|
|
|
|
query = query.eq('status', ticketStatusFilter.value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ticketTypeFilter != null) {
|
|
|
|
|
query = query.eq('ticket_type', ticketTypeFilter.value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (staffIdFilter != null) {
|
|
|
|
|
query = query.eq('staff_id', staffIdFilter);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (searchTerm != null && searchTerm.isNotEmpty) {
|
|
|
|
|
// Filtra sui campi della tabella principale O su quelli della tabella joinata
|
|
|
|
|
query = query.or('customer.name.ilike.%$searchTerm%');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final response = await query
|
|
|
|
|
.order('created_at', ascending: false)
|
|
|
|
|
.range(offset, offset + limit - 1);
|
|
|
|
|
|
|
|
|
|
return (response as List).map((map) => TicketModel.fromMap(map)).toList();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
throw Exception('$e');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Stream dei ticket che necessitano attenzione (es. in scadenza oggi o in ritardo)
|
|
|
|
|
Stream<List<TicketModel>> getAttentionNeededTicketsStream() {
|
|
|
|
|
return _supabase
|
|
|
|
|
.from(_tableName)
|
|
|
|
|
.stream(primaryKey: ['id'])
|
|
|
|
|
.eq('store_id', GetIt.I.get<SessionCubit>().state.currentStore!.id!)
|
|
|
|
|
// Purtroppo lo stream accetta solo filtri base, quindi ci facciamo
|
|
|
|
|
// mandare i dati e li filtriamo con la potenza di Dart!
|
|
|
|
|
.limit(300)
|
|
|
|
|
.map((listOfMaps) {
|
|
|
|
|
final now = DateTime.now();
|
|
|
|
|
final endOfToday = DateTime(now.year, now.month, now.day, 23, 59, 59);
|
|
|
|
|
|
|
|
|
|
// 1. Mappiamo tutto in TicketModel
|
|
|
|
|
final allStoreTickets = listOfMaps
|
|
|
|
|
.map((map) => TicketModel.fromMap(map))
|
|
|
|
|
.toList();
|
|
|
|
|
|
|
|
|
|
// 2. Filtriamo in memoria!
|
|
|
|
|
final urgentTickets = allStoreTickets.where((ticket) {
|
|
|
|
|
// Escludiamo quelli già chiusi o consegnati
|
|
|
|
|
if (ticket.ticketStatus == TicketStatus.closed ||
|
|
|
|
|
ticket.ticketStatus == TicketStatus.ready) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Se c'è una data di consegna stimata ed è <= a stasera, è urgente!
|
|
|
|
|
if (ticket.estimatedDeliveryAt != null) {
|
|
|
|
|
return ticket.estimatedDeliveryAt!.isBefore(endOfToday);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}).toList();
|
|
|
|
|
|
|
|
|
|
// 3. Li ordiniamo mettendo i più vecchi/urgenti in cima
|
|
|
|
|
urgentTickets.sort(
|
|
|
|
|
(a, b) => a.estimatedDeliveryAt!.compareTo(b.estimatedDeliveryAt!),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return urgentTickets;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Recupera un ticket specifico CON TUTTE LE RELAZIONI espanse (Cliente e Modelli)
|
|
|
|
|
/// Questa è la vera magia di Supabase!
|
|
|
|
|
Future<TicketModel> getTicketById(String ticketId) async {
|
|
|
|
|
try {
|
|
|
|
|
// Usiamo i nomi esatti delle Foreign Key che hai definito nell'SQL!
|
|
|
|
|
final response = await _supabase
|
|
|
|
|
.from(_tableName)
|
|
|
|
|
.select('''
|
|
|
|
|
*,
|
|
|
|
|
customer (*),
|
|
|
|
|
target_model:model!ticket_model_id_1_fkey (*),
|
|
|
|
|
source_model:model!ticket_model_id_2_fkey (*),
|
|
|
|
|
created_by:staff_member!ticket_staff_id_fkey (*),
|
|
|
|
|
assigned_to:staff_member!ticket_assigned_to_id_fkey (*),
|
2026-05-18 12:00:07 +02:00
|
|
|
shipment_document (*, attachments (*)),
|
2026-05-07 16:28:01 +02:00
|
|
|
''')
|
|
|
|
|
.eq('id', ticketId)
|
|
|
|
|
.single();
|
|
|
|
|
|
|
|
|
|
return TicketModel.fromMap(response);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
throw Exception('Errore nel recupero del dettaglio ticket: $e');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-16 14:30:23 +02:00
|
|
|
// Future<String> generateTicketReference(String companyId) async {
|
|
|
|
|
// final response = await Supabase.instance.client.rpc(
|
|
|
|
|
// 'get_next_document_number',
|
|
|
|
|
// params: {'p_company_id': companyId, 'p_doc_type': 'ticket'},
|
|
|
|
|
// );
|
2026-05-10 14:09:57 +02:00
|
|
|
|
2026-05-16 14:30:23 +02:00
|
|
|
// // Estraiamo i dati dal JSON
|
|
|
|
|
// final int nextValue = response['next_value'];
|
|
|
|
|
// final String prefix = response['prefix'] ?? '';
|
2026-05-10 14:09:57 +02:00
|
|
|
|
2026-05-16 14:30:23 +02:00
|
|
|
// final year = DateTime.now().year; // 2026
|
2026-05-10 14:09:57 +02:00
|
|
|
|
2026-05-16 14:30:23 +02:00
|
|
|
// // Formattazione con zeri iniziali (es. 000125)
|
|
|
|
|
// final paddedNumber = nextValue.toString().padLeft(6, '0');
|
2026-05-10 14:09:57 +02:00
|
|
|
|
2026-05-16 14:30:23 +02:00
|
|
|
// // Costruiamo la stringa. Se c'è un prefisso mette "TCK-2026-000125",
|
|
|
|
|
// // altrimenti solo "2026-000125"
|
|
|
|
|
// if (prefix.isNotEmpty) {
|
|
|
|
|
// return '$prefix-$year-$paddedNumber';
|
|
|
|
|
// } else {
|
|
|
|
|
// return '$year-$paddedNumber';
|
|
|
|
|
// }
|
|
|
|
|
// }
|
2026-05-10 14:09:57 +02:00
|
|
|
|
|
|
|
|
/// Salva il ticket
|
|
|
|
|
Future<TicketModel> insertTicket(TicketModel ticket) async {
|
|
|
|
|
if (ticket.id != null) {
|
|
|
|
|
throw Exception('Impossibile creare un ticket esistente, id not null');
|
|
|
|
|
}
|
2026-05-07 16:28:01 +02:00
|
|
|
try {
|
2026-05-10 14:09:57 +02:00
|
|
|
final ticketToSave = ticket.copyWith(
|
2026-05-16 14:30:23 +02:00
|
|
|
referenceId: await _documentSequenceRepository.getNextDocumentNumber(
|
|
|
|
|
DocumentType.ticket.name,
|
|
|
|
|
),
|
2026-05-10 14:09:57 +02:00
|
|
|
);
|
2026-05-07 16:28:01 +02:00
|
|
|
final response = await _supabase
|
|
|
|
|
.from(_tableName)
|
2026-05-10 14:09:57 +02:00
|
|
|
.insert(ticketToSave.toMap())
|
2026-05-07 16:28:01 +02:00
|
|
|
.select()
|
|
|
|
|
.single();
|
|
|
|
|
|
|
|
|
|
return TicketModel.fromMap(response);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
throw Exception('Errore nella creazione del ticket: $e');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Aggiorna un ticket esistente
|
|
|
|
|
Future<TicketModel> updateTicket(TicketModel ticket) async {
|
|
|
|
|
if (ticket.id == null) {
|
|
|
|
|
throw Exception('Impossibile aggiornare un ticket senza ID');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
final response = await _supabase
|
|
|
|
|
.from(_tableName)
|
|
|
|
|
.update(ticket.toMap())
|
|
|
|
|
.eq('id', ticket.id!)
|
|
|
|
|
.select()
|
|
|
|
|
.single();
|
|
|
|
|
|
|
|
|
|
return TicketModel.fromMap(response);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
throw Exception('Errore nell\'aggiornamento del ticket: $e');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Elimina (o annulla) un ticket
|
|
|
|
|
Future<void> deleteTicket(String ticketId) async {
|
|
|
|
|
try {
|
|
|
|
|
await _supabase.from(_tableName).delete().eq('id', ticketId);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
throw Exception('Errore nell\'eliminazione del ticket: $e');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|