feat-tickets #14
@@ -13,7 +13,6 @@ import 'package:shared_preferences/shared_preferences.dart';
|
|||||||
import 'package:flux/features/operations/models/operation_model.dart';
|
import 'package:flux/features/operations/models/operation_model.dart';
|
||||||
import 'package:flux/features/attachments/models/attachment_model.dart';
|
import 'package:flux/features/attachments/models/attachment_model.dart';
|
||||||
import 'package:pdf/widgets.dart' as pw;
|
import 'package:pdf/widgets.dart' as pw;
|
||||||
import 'package:pdf/pdf.dart' as p; // Se ti serve formattazione core
|
|
||||||
import 'package:pdfx/pdfx.dart' as px; // Isoliamo pdfx
|
import 'package:pdfx/pdfx.dart' as px; // Isoliamo pdfx
|
||||||
|
|
||||||
class _ExportItem {
|
class _ExportItem {
|
||||||
@@ -281,7 +280,7 @@ class _OperationFilesSectionState extends State<OperationFilesSection> {
|
|||||||
if (fileBytes == null) continue;
|
if (fileBytes == null) continue;
|
||||||
|
|
||||||
// Recuperiamo il nome che l'utente ha (magari) già impostato
|
// Recuperiamo il nome che l'utente ha (magari) già impostato
|
||||||
final baseName = file.name ?? 'Documento';
|
final baseName = file.name;
|
||||||
|
|
||||||
if (file.extension == 'pdf') {
|
if (file.extension == 'pdf') {
|
||||||
final document = await px.PdfDocument.openData(fileBytes);
|
final document = await px.PdfDocument.openData(fileBytes);
|
||||||
|
|||||||
73
lib/features/tickets/blocs/ticket_list_cubit.dart
Normal file
73
lib/features/tickets/blocs/ticket_list_cubit.dart
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flux/features/tickets/models/ticket_model.dart';
|
||||||
|
import 'package:flux/features/tickets/data/ticket_repository.dart';
|
||||||
|
import 'package:get_it/get_it.dart';
|
||||||
|
import 'ticket_list_state.dart';
|
||||||
|
|
||||||
|
class TicketListCubit extends Cubit<TicketListState> {
|
||||||
|
final TicketRepository _repository = GetIt.I.get<TicketRepository>();
|
||||||
|
static const int _limit = 20; // Paginazione a blocchi di 20
|
||||||
|
|
||||||
|
TicketListCubit() : super(const TicketListState()) {
|
||||||
|
fetchTickets(reset: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recupera i ticket. Se reset = true, svuota la lista e riparte da offset 0.
|
||||||
|
Future<void> fetchTickets({bool reset = false}) async {
|
||||||
|
if (state.isLoading) return;
|
||||||
|
if (!reset && state.hasReachedMax) return;
|
||||||
|
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
isLoading: true,
|
||||||
|
errorMessage: '',
|
||||||
|
tickets: reset ? [] : state.tickets,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final currentOffset = reset ? 0 : state.tickets.length;
|
||||||
|
|
||||||
|
final newTickets = await _repository.fetchStoreTickets(
|
||||||
|
offset: currentOffset,
|
||||||
|
limit: _limit,
|
||||||
|
searchTerm: state.searchTerm,
|
||||||
|
dateRange: state.dateRange,
|
||||||
|
statusFilter: state.statusFilter,
|
||||||
|
);
|
||||||
|
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
tickets: reset ? newTickets : [...state.tickets, ...newTickets],
|
||||||
|
isLoading: false,
|
||||||
|
hasReachedMax: newTickets.length < _limit,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
emit(state.copyWith(isLoading: false, errorMessage: e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Aggiorna i filtri e ricarica tutto da zero
|
||||||
|
void updateFilters({
|
||||||
|
String? searchTerm,
|
||||||
|
DateTimeRange? dateRange,
|
||||||
|
TicketStatus? statusFilter,
|
||||||
|
bool clearSearch = false,
|
||||||
|
bool clearDate = false,
|
||||||
|
bool clearStatus = false,
|
||||||
|
}) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
searchTerm: searchTerm,
|
||||||
|
dateRange: dateRange,
|
||||||
|
statusFilter: statusFilter,
|
||||||
|
clearSearch: clearSearch,
|
||||||
|
clearDate: clearDate,
|
||||||
|
clearStatus: clearStatus,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
fetchTickets(reset: true); // Applica i filtri e ricarica
|
||||||
|
}
|
||||||
|
}
|
||||||
59
lib/features/tickets/blocs/ticket_list_state.dart
Normal file
59
lib/features/tickets/blocs/ticket_list_state.dart
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flux/features/tickets/models/ticket_model.dart';
|
||||||
|
|
||||||
|
class TicketListState extends Equatable {
|
||||||
|
final List<TicketModel> tickets;
|
||||||
|
final bool isLoading;
|
||||||
|
final bool hasReachedMax;
|
||||||
|
final String errorMessage;
|
||||||
|
|
||||||
|
// Filtri attivi
|
||||||
|
final String? searchTerm;
|
||||||
|
final DateTimeRange? dateRange;
|
||||||
|
final TicketStatus? statusFilter;
|
||||||
|
|
||||||
|
const TicketListState({
|
||||||
|
this.tickets = const [],
|
||||||
|
this.isLoading = false,
|
||||||
|
this.hasReachedMax = false,
|
||||||
|
this.errorMessage = '',
|
||||||
|
this.searchTerm,
|
||||||
|
this.dateRange,
|
||||||
|
this.statusFilter,
|
||||||
|
});
|
||||||
|
|
||||||
|
TicketListState copyWith({
|
||||||
|
List<TicketModel>? tickets,
|
||||||
|
bool? isLoading,
|
||||||
|
bool? hasReachedMax,
|
||||||
|
String? errorMessage,
|
||||||
|
String? searchTerm,
|
||||||
|
DateTimeRange? dateRange,
|
||||||
|
TicketStatus? statusFilter,
|
||||||
|
bool clearSearch = false,
|
||||||
|
bool clearDate = false,
|
||||||
|
bool clearStatus = false,
|
||||||
|
}) {
|
||||||
|
return TicketListState(
|
||||||
|
tickets: tickets ?? this.tickets,
|
||||||
|
isLoading: isLoading ?? this.isLoading,
|
||||||
|
hasReachedMax: hasReachedMax ?? this.hasReachedMax,
|
||||||
|
errorMessage: errorMessage ?? this.errorMessage,
|
||||||
|
searchTerm: clearSearch ? null : (searchTerm ?? this.searchTerm),
|
||||||
|
dateRange: clearDate ? null : (dateRange ?? this.dateRange),
|
||||||
|
statusFilter: clearStatus ? null : (statusFilter ?? this.statusFilter),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [
|
||||||
|
tickets,
|
||||||
|
isLoading,
|
||||||
|
hasReachedMax,
|
||||||
|
errorMessage,
|
||||||
|
searchTerm,
|
||||||
|
dateRange,
|
||||||
|
statusFilter,
|
||||||
|
];
|
||||||
|
}
|
||||||
235
lib/features/tickets/data/ticket_repository.dart
Normal file
235
lib/features/tickets/data/ticket_repository.dart
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
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 (*),
|
||||||
|
staff (*),
|
||||||
|
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 (*),
|
||||||
|
staff (*),
|
||||||
|
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.status == TicketStatus.closed ||
|
||||||
|
ticket.status == 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> getTicketWithDetails(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 (*),
|
||||||
|
staff:staff_member!ticket_staff_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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
345
lib/features/tickets/models/ticket_model.dart
Normal file
345
lib/features/tickets/models/ticket_model.dart
Normal file
@@ -0,0 +1,345 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:flux/core/utils/extensions.dart';
|
||||||
|
|
||||||
|
/// Enum per il tipo di ticket
|
||||||
|
enum TicketType {
|
||||||
|
repair('repair', 'Riparazione'),
|
||||||
|
softwareSetup('software_setup', 'Setup software'),
|
||||||
|
dataTransfer('data_transfer', 'Trasferimento dati'),
|
||||||
|
operationTicket('operation_ticket', 'Ticket di operazione'),
|
||||||
|
other('other', 'Altro');
|
||||||
|
|
||||||
|
final String value;
|
||||||
|
final String displayValue;
|
||||||
|
const TicketType(this.value, this.displayValue);
|
||||||
|
|
||||||
|
static TicketType fromString(String val) {
|
||||||
|
return TicketType.values.firstWhere(
|
||||||
|
(e) => e.value == val,
|
||||||
|
orElse: () => TicketType.other,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enum per lo stato del ticket
|
||||||
|
enum TicketStatus {
|
||||||
|
open('open', 'Aperto'),
|
||||||
|
inProgress('in_progress', 'In corso'),
|
||||||
|
waitingForParts('waiting_for_parts', 'In attesa di ricambi'),
|
||||||
|
ready('ready', 'Pronto'),
|
||||||
|
closed('closed', 'Chiuso'),
|
||||||
|
waitingForShipping('waiting_for_shipping', 'In attesa di spedire'),
|
||||||
|
waitingForReturn('waiting_for_return', 'In attesa di ritorno');
|
||||||
|
|
||||||
|
final String value;
|
||||||
|
final String displayValue;
|
||||||
|
const TicketStatus(this.value, this.displayValue);
|
||||||
|
|
||||||
|
static TicketStatus? fromString(String? val) {
|
||||||
|
if (val == null) return null;
|
||||||
|
return TicketStatus.values.firstWhere(
|
||||||
|
(e) => e.value == val,
|
||||||
|
orElse: () => TicketStatus.open,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enum per il risultato del ticket (OK / KO)
|
||||||
|
enum TicketResult {
|
||||||
|
success('success', 'Risolto (OK)'),
|
||||||
|
failure('failure', 'Non Risolto (KO)');
|
||||||
|
|
||||||
|
final String value;
|
||||||
|
final String displayValue;
|
||||||
|
const TicketResult(this.value, this.displayValue);
|
||||||
|
|
||||||
|
static TicketResult? fromString(String? val) {
|
||||||
|
if (val == null) return null;
|
||||||
|
return TicketResult.values.firstWhere(
|
||||||
|
(e) => e.value == val,
|
||||||
|
orElse: () => TicketResult.success,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enum per il tipo di garanzia
|
||||||
|
enum WarrantyType {
|
||||||
|
manufacturerWarranty('manufacturer_warranty', 'Garanzia produttore'),
|
||||||
|
providerWarranty('provider_warranty', 'Garanzia gestore'),
|
||||||
|
internalWarranty('internal_warranty', 'Garanzia interna'),
|
||||||
|
noWarranty('no_warranty', 'Fuori garanzia');
|
||||||
|
|
||||||
|
final String value;
|
||||||
|
final String displayValue;
|
||||||
|
const WarrantyType(this.value, this.displayValue);
|
||||||
|
|
||||||
|
static WarrantyType? fromString(String? val) {
|
||||||
|
return WarrantyType.values.firstWhere(
|
||||||
|
(e) => e.value == val,
|
||||||
|
orElse: () => WarrantyType.noWarranty,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TicketModel extends Equatable {
|
||||||
|
final String? id; // Null se non ancora salvato
|
||||||
|
final DateTime? createdAt;
|
||||||
|
final String companyId;
|
||||||
|
final String? storeId;
|
||||||
|
final String? customerId;
|
||||||
|
final String? targetModelId;
|
||||||
|
final String? targetSn;
|
||||||
|
final String? sourceModelId;
|
||||||
|
final String? sourceSn;
|
||||||
|
final double customerPrice;
|
||||||
|
final double internalCost;
|
||||||
|
final DateTime? closedAt;
|
||||||
|
final DateTime? returnedAt;
|
||||||
|
final String request;
|
||||||
|
final String? staffId;
|
||||||
|
final WarrantyType? warrantyType;
|
||||||
|
final String? publicNotes;
|
||||||
|
final String? internalNotes;
|
||||||
|
final int? referenceNumber;
|
||||||
|
final String? alternativePhoneNumber;
|
||||||
|
final bool hasCourtesyDevice;
|
||||||
|
final TicketType ticketType;
|
||||||
|
final TicketStatus? status;
|
||||||
|
final DateTime? estimatedDeliveryAt;
|
||||||
|
final TicketResult? result;
|
||||||
|
final String? resolutionNotes;
|
||||||
|
final String? legacyId;
|
||||||
|
final String? customerName;
|
||||||
|
final String? targetModelName;
|
||||||
|
final String? sourceModelName;
|
||||||
|
final String? staffName;
|
||||||
|
|
||||||
|
const TicketModel({
|
||||||
|
this.id,
|
||||||
|
this.createdAt,
|
||||||
|
required this.companyId,
|
||||||
|
this.storeId,
|
||||||
|
this.customerId,
|
||||||
|
this.targetModelId,
|
||||||
|
this.targetSn,
|
||||||
|
this.sourceModelId,
|
||||||
|
this.sourceSn,
|
||||||
|
this.customerPrice = 0.0,
|
||||||
|
this.internalCost = 0.0,
|
||||||
|
this.closedAt,
|
||||||
|
this.returnedAt,
|
||||||
|
this.request = '',
|
||||||
|
this.staffId,
|
||||||
|
this.warrantyType,
|
||||||
|
this.publicNotes,
|
||||||
|
this.internalNotes,
|
||||||
|
this.referenceNumber,
|
||||||
|
this.alternativePhoneNumber,
|
||||||
|
this.hasCourtesyDevice = false,
|
||||||
|
required this.ticketType,
|
||||||
|
this.status,
|
||||||
|
this.estimatedDeliveryAt,
|
||||||
|
this.result,
|
||||||
|
this.resolutionNotes,
|
||||||
|
this.legacyId,
|
||||||
|
this.customerName,
|
||||||
|
this.targetModelName,
|
||||||
|
this.sourceModelName,
|
||||||
|
this.staffName,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Factory per creare un ticket vuoto (utile per i form di creazione)
|
||||||
|
factory TicketModel.empty({required String companyId, String? storeId}) {
|
||||||
|
return TicketModel(
|
||||||
|
companyId: companyId,
|
||||||
|
storeId: storeId,
|
||||||
|
ticketType: TicketType.repair, // Valore di default
|
||||||
|
status: TicketStatus.open,
|
||||||
|
customerPrice: 0.0,
|
||||||
|
internalCost: 0.0,
|
||||||
|
hasCourtesyDevice: false,
|
||||||
|
request: '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TicketModel copyWith({
|
||||||
|
String? id,
|
||||||
|
DateTime? createdAt,
|
||||||
|
String? companyId,
|
||||||
|
String? storeId,
|
||||||
|
String? customerId,
|
||||||
|
String? targetModelId,
|
||||||
|
String? targetSn,
|
||||||
|
String? sourceModelId,
|
||||||
|
String? sourceSn,
|
||||||
|
double? customerPrice,
|
||||||
|
double? internalCost,
|
||||||
|
DateTime? closedAt,
|
||||||
|
DateTime? returnedAt,
|
||||||
|
String? request,
|
||||||
|
String? staffId,
|
||||||
|
WarrantyType? warrantyType,
|
||||||
|
String? publicNotes,
|
||||||
|
String? internalNotes,
|
||||||
|
int? referenceNumber,
|
||||||
|
String? alternativePhoneNumber,
|
||||||
|
bool? hasCourtesyDevice,
|
||||||
|
TicketType? ticketType,
|
||||||
|
TicketStatus? status,
|
||||||
|
DateTime? estimatedDeliveryAt,
|
||||||
|
TicketResult? result,
|
||||||
|
String? resolutionNotes,
|
||||||
|
String? legacyId,
|
||||||
|
String? customerName,
|
||||||
|
String? targetModelName,
|
||||||
|
String? sourceModelName,
|
||||||
|
String? staffName,
|
||||||
|
}) {
|
||||||
|
return TicketModel(
|
||||||
|
id: id ?? this.id,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
companyId: companyId ?? this.companyId,
|
||||||
|
storeId: storeId ?? this.storeId,
|
||||||
|
customerId: customerId ?? this.customerId,
|
||||||
|
targetModelId: targetModelId ?? this.targetModelId,
|
||||||
|
targetSn: targetSn ?? this.targetSn,
|
||||||
|
sourceModelId: sourceModelId ?? this.sourceModelId,
|
||||||
|
sourceSn: sourceSn ?? this.sourceSn,
|
||||||
|
customerPrice: customerPrice ?? this.customerPrice,
|
||||||
|
internalCost: internalCost ?? this.internalCost,
|
||||||
|
closedAt: closedAt ?? this.closedAt,
|
||||||
|
returnedAt: returnedAt ?? this.returnedAt,
|
||||||
|
request: request ?? this.request,
|
||||||
|
staffId: staffId ?? this.staffId,
|
||||||
|
warrantyType: warrantyType ?? this.warrantyType,
|
||||||
|
publicNotes: publicNotes ?? this.publicNotes,
|
||||||
|
internalNotes: internalNotes ?? this.internalNotes,
|
||||||
|
referenceNumber: referenceNumber ?? this.referenceNumber,
|
||||||
|
alternativePhoneNumber:
|
||||||
|
alternativePhoneNumber ?? this.alternativePhoneNumber,
|
||||||
|
hasCourtesyDevice: hasCourtesyDevice ?? this.hasCourtesyDevice,
|
||||||
|
ticketType: ticketType ?? this.ticketType,
|
||||||
|
status: status ?? this.status,
|
||||||
|
estimatedDeliveryAt: estimatedDeliveryAt ?? this.estimatedDeliveryAt,
|
||||||
|
result: result ?? this.result,
|
||||||
|
resolutionNotes: resolutionNotes ?? this.resolutionNotes,
|
||||||
|
legacyId: legacyId ?? this.legacyId,
|
||||||
|
customerName: customerName ?? this.customerName,
|
||||||
|
targetModelName: targetModelName ?? this.targetModelName,
|
||||||
|
sourceModelName: sourceModelName ?? this.sourceModelName,
|
||||||
|
staffName: staffName ?? this.staffName,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserializzazione da Supabase
|
||||||
|
factory TicketModel.fromMap(Map<String, dynamic> map) {
|
||||||
|
return TicketModel(
|
||||||
|
id: map['id'] as String,
|
||||||
|
createdAt: map['created_at'] != null
|
||||||
|
? DateTime.parse(map['created_at']).toLocal()
|
||||||
|
: null,
|
||||||
|
companyId: map['company_id'] as String,
|
||||||
|
storeId: map['store_id'] as String?,
|
||||||
|
customerId: map['customer_id'] as String?,
|
||||||
|
targetModelId: map['target_model_id'] as String?,
|
||||||
|
targetSn: map['target_sn'] as String?,
|
||||||
|
sourceModelId: map['source_model_id'] as String?,
|
||||||
|
sourceSn: map['source_sn'] as String?,
|
||||||
|
// Fix per i field numerici di Postgres che potrebbero arrivare come int o double
|
||||||
|
customerPrice: (map['customer_price'] as num?)?.toDouble() ?? 0.0,
|
||||||
|
internalCost: (map['internal_cost'] as num?)?.toDouble() ?? 0.0,
|
||||||
|
closedAt: map['closed_at'] != null
|
||||||
|
? DateTime.parse(map['closed_at']).toLocal()
|
||||||
|
: null,
|
||||||
|
returnedAt: map['returned_at'] != null
|
||||||
|
? DateTime.parse(map['returned_at']).toLocal()
|
||||||
|
: null,
|
||||||
|
request: map['request'] as String? ?? '',
|
||||||
|
staffId: map['staff_id'] as String?,
|
||||||
|
warrantyType: WarrantyType.fromString(map['warranty_type'] as String?),
|
||||||
|
publicNotes: map['public_notes'] as String?,
|
||||||
|
internalNotes: map['internal_notes'] as String?,
|
||||||
|
referenceNumber: map['reference_number'] as int?,
|
||||||
|
alternativePhoneNumber: map['alternative_phone_number'] as String?,
|
||||||
|
hasCourtesyDevice: map['has_courtesy_device'] as bool? ?? false,
|
||||||
|
ticketType: TicketType.fromString(map['ticket_type'] as String),
|
||||||
|
status: TicketStatus.fromString(map['status'] as String?),
|
||||||
|
estimatedDeliveryAt: map['estimated_delivery_at'] != null
|
||||||
|
? DateTime.parse(map['estimated_delivery_at']).toLocal()
|
||||||
|
: null,
|
||||||
|
result: TicketResult.fromString(map['result'] as String?),
|
||||||
|
resolutionNotes: map['resolution_notes'] as String?,
|
||||||
|
legacyId: map['legacy_id'] as String?,
|
||||||
|
customerName: (map['customer']?['name'] as String?).myFormat(),
|
||||||
|
targetModelName: (map['target_model']?['name_with_brand'] as String?)
|
||||||
|
?.myFormat(),
|
||||||
|
sourceModelName: (map['source_model']?['name_with_brand'] as String?)
|
||||||
|
?.myFormat(),
|
||||||
|
staffName: (map['staff']?['name'] as String?).myFormat(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serializzazione per Supabase
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
return {
|
||||||
|
if (id != null) 'id': id,
|
||||||
|
'company_id': companyId,
|
||||||
|
'store_id': storeId,
|
||||||
|
'customer_id': customerId,
|
||||||
|
'target_model_id': targetModelId,
|
||||||
|
'target_sn': targetSn,
|
||||||
|
'source_model_id': sourceModelId,
|
||||||
|
'source_sn': sourceSn,
|
||||||
|
'customer_price': customerPrice,
|
||||||
|
'internal_cost': internalCost,
|
||||||
|
if (closedAt != null) 'closed_at': closedAt!.toUtc().toIso8601String(),
|
||||||
|
if (returnedAt != null)
|
||||||
|
'returned_at': returnedAt!.toUtc().toIso8601String(),
|
||||||
|
'request': request,
|
||||||
|
'staff_id': staffId,
|
||||||
|
'warranty_type': warrantyType,
|
||||||
|
'public_notes': publicNotes,
|
||||||
|
'internal_notes': internalNotes,
|
||||||
|
'alternative_phone_number': alternativePhoneNumber,
|
||||||
|
'has_courtesy_device': hasCourtesyDevice,
|
||||||
|
'ticket_type': ticketType.value,
|
||||||
|
if (status != null) 'status': status!.value,
|
||||||
|
if (estimatedDeliveryAt != null)
|
||||||
|
'estimated_delivery_at': estimatedDeliveryAt!.toUtc().toIso8601String(),
|
||||||
|
if (result != null) 'result': result!.value,
|
||||||
|
'resolution_notes': resolutionNotes,
|
||||||
|
'legacy_id': legacyId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [
|
||||||
|
id,
|
||||||
|
createdAt,
|
||||||
|
companyId,
|
||||||
|
storeId,
|
||||||
|
customerId,
|
||||||
|
targetModelId,
|
||||||
|
targetSn,
|
||||||
|
sourceModelId,
|
||||||
|
sourceSn,
|
||||||
|
customerPrice,
|
||||||
|
internalCost,
|
||||||
|
closedAt,
|
||||||
|
returnedAt,
|
||||||
|
request,
|
||||||
|
staffId,
|
||||||
|
warrantyType,
|
||||||
|
publicNotes,
|
||||||
|
internalNotes,
|
||||||
|
referenceNumber,
|
||||||
|
alternativePhoneNumber,
|
||||||
|
hasCourtesyDevice,
|
||||||
|
ticketType,
|
||||||
|
status,
|
||||||
|
estimatedDeliveryAt,
|
||||||
|
result,
|
||||||
|
resolutionNotes,
|
||||||
|
legacyId,
|
||||||
|
];
|
||||||
|
}
|
||||||
43
lib/features/tickets/models/ticket_status_extension.dart
Normal file
43
lib/features/tickets/models/ticket_status_extension.dart
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flux/features/tickets/models/ticket_model.dart';
|
||||||
|
|
||||||
|
extension TicketStatusVisuals on TicketStatus {
|
||||||
|
Color get color {
|
||||||
|
switch (this) {
|
||||||
|
case TicketStatus.open:
|
||||||
|
return Colors.blueGrey;
|
||||||
|
case TicketStatus.waitingForParts:
|
||||||
|
return Colors.amber.shade700;
|
||||||
|
case TicketStatus.inProgress:
|
||||||
|
return Colors.blue;
|
||||||
|
case TicketStatus.waitingForShipping:
|
||||||
|
// Il tuo rosa storico!
|
||||||
|
return Colors.pinkAccent;
|
||||||
|
case TicketStatus.waitingForReturn:
|
||||||
|
return Colors.purpleAccent;
|
||||||
|
case TicketStatus.ready:
|
||||||
|
return Colors.green;
|
||||||
|
case TicketStatus.closed:
|
||||||
|
return Colors.grey.shade400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IconData get icon {
|
||||||
|
switch (this) {
|
||||||
|
case TicketStatus.open:
|
||||||
|
return Icons.inbox;
|
||||||
|
case TicketStatus.waitingForParts:
|
||||||
|
return Icons.hourglass_empty;
|
||||||
|
case TicketStatus.inProgress:
|
||||||
|
return Icons.build;
|
||||||
|
case TicketStatus.waitingForShipping:
|
||||||
|
return Icons.local_shipping_outlined;
|
||||||
|
case TicketStatus.waitingForReturn:
|
||||||
|
return Icons.undo;
|
||||||
|
case TicketStatus.ready:
|
||||||
|
return Icons.check_circle_outline;
|
||||||
|
case TicketStatus.closed:
|
||||||
|
return Icons.lock_outline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user