feat-tickets (#14)
Some checks failed
Deploy to Cloudflare Pages / build-and-deploy (push) Has been cancelled

Reviewed-on: #14
Co-authored-by: mark-cachy <marco@catelli.it>
Co-committed-by: mark-cachy <marco@catelli.it>
This commit is contained in:
2026-05-07 16:28:01 +02:00
committed by brontomark
parent 94ad524bae
commit 7d03d0dea5
38 changed files with 3594 additions and 1486 deletions

View File

@@ -0,0 +1,238 @@
import 'package:flutter/material.dart';
import 'package:flux/core/blocs/session/session_cubit.dart';
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>();
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 (*),
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 (*),
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 (*),
''')
.eq('id', ticketId)
.single();
return TicketModel.fromMap(response);
} catch (e) {
throw Exception('Errore nel recupero del dettaglio ticket: $e');
}
}
/// Salva il ticket con upsert
Future<TicketModel> saveTicket(TicketModel ticket) async {
try {
final response = await _supabase
.from(_tableName)
.upsert(ticket.toMap())
.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');
}
}
}