This commit is contained in:
2026-05-12 12:36:50 +02:00
parent 2aab70aec5
commit 216fd85888
6 changed files with 592 additions and 3 deletions

View File

@@ -0,0 +1,57 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/features/tracking/data/tracking_repository.dart';
import 'package:flux/features/tracking/models/tracking_model.dart';
// Stati base: initial, loading, loaded, error
class TrackingState {
final bool isLoading;
final List<TrackingModel> logs;
TrackingState({this.isLoading = false, this.logs = const []});
}
class TrackingCubit extends Cubit<TrackingState> {
final TrackingRepository _repo;
final String parentId;
final TrackingParentType parentType;
final String companyId;
TrackingCubit({
required TrackingRepository repo,
required this.parentId,
required this.parentType,
required this.companyId,
}) : _repo = repo,
super(TrackingState()) {
loadTrackings();
}
Future<void> loadTrackings() async {
emit(TrackingState(isLoading: true, logs: state.logs));
final trackings = await _repo.getTrackingsByParent(
parentId: parentId,
parentType: parentType,
);
emit(TrackingState(isLoading: false, logs: trackings));
}
Future<void> addManualNote(
String message,
bool isInternal, {
String? staffId,
}) async {
// Aggiungiamo un feedback visivo immediato (Optimistic UI) se vogliamo,
// oppure semplicemente mostriamo il loading
await _repo.logQuickEvent(
companyId: companyId,
message: message,
type: TrackingType.manualNote,
parentId: parentId,
parentType: parentType,
staffId: staffId,
isInternal: isInternal,
);
// Ricarichiamo la lista fresca dal server
await loadTrackings();
}
}

View File

@@ -0,0 +1,57 @@
import 'package:flux/features/tracking/models/tracking_model.dart';
import 'package:get_it/get_it.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
class TrackingRepository {
final SupabaseClient _supabase = GetIt.I.get<SupabaseClient>();
TrackingRepository();
/// Recupera la cronologia di un'entità (Ticket o Operazione)
Future<List<TrackingModel>> getTrackingsByParent({
required String parentId, // <-- Reso obbligatorio
required TrackingParentType parentType, // <-- Reso obbligatorio
}) async {
// Facciamo la query con la JOIN per recuperare il nome dello staff al volo
final response = await _supabase
.from('tracking')
.select('*, staff_member(name)')
.eq('parent_id', parentId)
.eq('parent_type', parentType.name)
.order(
'created_at',
ascending: true,
); // ascending: true per avere la timeline dall'alto (vecchi) al basso (nuovi)
return response.map((map) => TrackingModel.fromMap(map)).toList();
}
/// Inserisce un nuovo evento di tracking
Future<void> logEvent(TrackingModel tracking) async {
await _supabase.from('tracking').insert(tracking.toMap());
}
/// Metodo helper rapido per loggare un cambio di stato o una nota
Future<void> logQuickEvent({
required String companyId,
required String message,
required TrackingType type,
required String parentId, // <-- Reso obbligatorio
required TrackingParentType parentType, // <-- Reso obbligatorio
String? staffId,
bool isInternal = true,
}) async {
final log = TrackingModel(
createdAt:
DateTime.now(), // Questo verrà ignorato dal toMap in fase di insert, ma serve al modello
companyId: companyId,
staffId: staffId,
parentId: parentId,
parentType: parentType,
eventType: type,
isInternal: isInternal,
message: message,
);
await logEvent(log);
}
}

View File

@@ -0,0 +1,132 @@
import 'package:equatable/equatable.dart';
enum TrackingType {
statusChange,
manualNote,
systemAlert,
customerContact,
assignment;
static TrackingType fromString(String value) {
return TrackingType.values.firstWhere(
(e) => e.name == value,
orElse: () => TrackingType.manualNote,
);
}
}
enum TrackingParentType {
ticket,
operation;
String get value => name;
static TrackingParentType fromString(String val) {
return TrackingParentType.values.firstWhere(
(e) => e.name == val,
orElse: () => TrackingParentType.ticket, // Default di sicurezza
);
}
}
class TrackingModel extends Equatable {
final String? id;
final DateTime createdAt;
final String companyId;
final String? staffId;
final String? staffName; // Per non fare mille join, lo prendiamo dal repo
final String parentId;
final TrackingParentType parentType;
final TrackingType eventType;
final bool isInternal;
final String message;
const TrackingModel({
this.id,
required this.createdAt,
required this.companyId,
this.staffId,
this.staffName,
required this.parentId,
required this.parentType,
required this.eventType,
required this.isInternal,
required this.message,
});
TrackingModel copyWith({
String? id,
DateTime? createdAt,
String? companyId,
String? staffId,
String? staffName,
TrackingParentType? parentType,
String? parentId,
TrackingType? eventType,
bool? isInternal,
String? message,
}) {
return TrackingModel(
id: id ?? this.id,
createdAt: createdAt ?? this.createdAt,
companyId: companyId ?? this.companyId,
staffId: staffId ?? this.staffId,
staffName: staffName ?? this.staffName,
parentId: parentId ?? this.parentId,
parentType: parentType ?? this.parentType,
eventType: eventType ?? this.eventType,
isInternal: isInternal ?? this.isInternal,
message: message ?? this.message,
);
}
factory TrackingModel.fromMap(Map<String, dynamic> map) {
return TrackingModel(
id: map['id'],
createdAt: DateTime.parse(map['created_at']),
companyId: map['company_id'],
staffId: map['staff_id'],
staffName: map['staff_member']?['name'], // Se fai la join su staff_member
parentId: map['parent_id'] as String,
parentType: TrackingParentType.fromString(map['parent_type']),
eventType: TrackingType.fromString(map['event_type']),
isInternal: map['is_internal'] ?? true,
message: map['message'],
);
}
Map<String, dynamic> toMap() {
final map = <String, dynamic>{
'company_id': companyId,
'staff_id': staffId,
'parent_id': parentId,
'parent_type': parentType.name,
'event_type': eventType.name,
'is_internal': isInternal,
'message': message,
};
// Aggiungiamo id e data SOLO se stiamo aggiornando un record esistente.
// In fase di creazione (insert), li omettiamo così Supabase usa i valori di default (gen_random_uuid e now()).
if (id != null) {
map['id'] = id;
map['created_at'] = createdAt.toIso8601String();
}
return map;
}
@override
List<Object?> get props => [
id,
createdAt,
companyId,
staffId,
staffName,
parentId,
parentType,
eventType,
isInternal,
message,
];
}