import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/enums_and_consts/consts.dart'; import 'package:flux/features/tasks/models/task_reminder_config.dart'; import 'package:flux/features/tasks/models/task_status.dart'; import 'package:get_it/get_it.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; // Sostituisci con i percorsi corretti di FLUX import 'package:flux/features/tasks/models/task_model.dart'; class TaskRepository { final SupabaseClient _supabase; TaskRepository({SupabaseClient? supabase}) : _supabase = supabase ?? Supabase.instance.client; // --- LOGICA REAL-TIME (Il Campanello) --- Stream watchCompanyTasks(String companyId) { // Usiamo un broadcast nel caso più bloc volessero ascoltarlo in futuro final controller = StreamController.broadcast(); final channel = _supabase.channel('public:tasks_company_$companyId'); channel .onPostgresChanges( event: PostgresChangeEvent.all, schema: 'public', table: Tables.tasks, filter: PostgresChangeFilter( type: PostgresChangeFilterType.eq, column: 'company_id', value: companyId, ), callback: (payload) { if (!controller.isClosed) { controller.add( null, ); // Suoniamo il campanello! Nessun dato, solo il "ding" } }, ) .subscribe(); // Quando il Cubit smette di ascoltare, puliamo il canale Supabase in automatico controller.onCancel = () { channel.unsubscribe(); controller.close(); }; return controller.stream; } // --- RECUPERO DEI TASK FILTRATI --- Future> getTasks({ required String companyId, String? storeId, String? staffId, List? statuses, int? limit, }) async { try { // 1. FASE FILTRI: Usa il join esplicito stile "Notes" var filterBuilder = _supabase .from(Tables.tasks) .select(''' *, task_assignments:${Tables.taskAssignments} ( ${Tables.staffMembers} (*) ) ''') .eq('company_id', companyId); if (storeId != null) { filterBuilder = filterBuilder.or( 'store_id.eq.$storeId,store_id.is.null', ); } if (staffId != null) { // Grazie al trigger, hai l'array pronto per il filtro senza impazzire! filterBuilder = filterBuilder.contains('assigned_to_ids', [staffId]); } if (statuses != null && statuses.isNotEmpty) { final statusValues = statuses.map((s) => s.toValue).toList(); filterBuilder = filterBuilder.inFilter('status', statusValues); } // 2. FASE TRASFORMAZIONI var transformBuilder = filterBuilder .order('due_date', ascending: true, nullsFirst: false) .order('created_at', ascending: false, nullsFirst: false); if (limit != null) { transformBuilder = transformBuilder.limit(limit); } // 3. ESECUZIONE DELLA QUERY final response = await transformBuilder; // 4. PARSING DEI DATI return (response as List).map((json) => TaskModel.fromMap(json)).toList(); } catch (e) { throw Exception('Errore nel recupero dei task: $e'); } } Future createTask({ required TaskModel task, required List assignedStaffIds, required String currentUserId, required List currentUserCustomReminders, TaskReminderConfig? managerForcedOverride, // Opzionale: l'avviso forzato del manager }) async { try { // 1. Inserimento del Task principale -> otteniamo il taskId // 2. Inserimento dei record in task_assignments final String taskId = task.id!; List> remindersToInsert = []; // 3. Recuperiamo i default degli ALTRI utenti assegnati final otherStaffIds = assignedStaffIds .where((id) => id != currentUserId) .toList(); List otherDefaults = []; if (otherStaffIds.isNotEmpty) { otherDefaults = await _supabase .from('staff_task_reminder_defaults') .select() .inFilter('staff_id', otherStaffIds); } // 4. CICLO DI COSTRUZIONE DELLA CODA REMINDER for (var staffId in assignedStaffIds) { // CASO A: È l'utente loggato che sta creando/partecipando al task if (staffId == currentUserId) { for (var config in currentUserCustomReminders) { final triggerAt = task.dueDate?.subtract( Duration(minutes: config.minutesBefore), ); if (triggerAt != null && triggerAt.isAfter(DateTime.now())) { remindersToInsert.add({ 'company_id': task.companyId, 'task_id': taskId, 'staff_id': currentUserId, 'minutes_before': config.minutesBefore, 'channel': config.channel, 'trigger_at': triggerAt.toIso8601String(), 'is_forced': false, }); } } } // CASO B: Sono gli altri assegnatari -> ereditano i loro default personali dal DB else { final staffRules = otherDefaults.where( (row) => row['staff_id'] == staffId, ); for (var rule in staffRules) { final minutesBefore = rule['minutes_before'] as int; final triggerAt = task.dueDate?.subtract( Duration(minutes: minutesBefore), ); if (triggerAt != null && triggerAt.isAfter(DateTime.now())) { remindersToInsert.add({ 'company_id': task.companyId, 'task_id': taskId, 'staff_id': staffId, 'minutes_before': minutesBefore, 'channel': rule['channel'], 'trigger_at': triggerAt.toIso8601String(), 'is_forced': false, }); } } } // CASO C: Il creatore ha impostato un avviso forzato (Override molto importante) if (managerForcedOverride != null && task.dueDate != null) { final triggerAt = task.dueDate!.subtract( Duration(minutes: managerForcedOverride.minutesBefore), ); if (triggerAt.isAfter(DateTime.now())) { remindersToInsert.add({ 'company_id': task.companyId, 'task_id': taskId, 'staff_id': staffId, // Lo beccheranno tutti 'minutes_before': managerForcedOverride.minutesBefore, 'channel': managerForcedOverride.channel, 'trigger_at': triggerAt.toIso8601String(), 'is_forced': true, // Chiude la possibilità di cancellarlo lato utente }); } } } // 5. Sparata unica di Bulk Insert su task_reminders if (remindersToInsert.isNotEmpty) { await _supabase.from('task_reminders').insert(remindersToInsert); } } catch (e) { throw Exception('Errore creazione task: $e'); } } // --- 3. AGGIORNAMENTO DEL TASK --- Future updateTask(TaskModel task) async { if (task.id == null) { throw Exception('ID Task mancante. Impossibile aggiornare.'); } try { final taskData = task.toMap(); taskData.remove( 'assigned_to_ids', ); // Sempre via l'array dal body principale // 1. Aggiorniamo i dati base del task (titolo, stato, scadenza, ecc.) await _supabase.from(Tables.tasks).update(taskData).eq('id', task.id!); // 2. Sincronizziamo in modo distruttivo gli assegnatari nella tabella di giunzione await _syncAssignments(task.id!, task.assignedToIds); // 3. Ritorniamo il modello fresco di DB return await getTaskById(task.id!); } catch (e) { throw Exception('Errore nell\'aggiornamento del task: $e'); } } Future createTask({ required TaskModel task, required List assignedStaffIds, required String currentUserId, required List currentUserCustomReminders, TaskReminderConfig? managerForcedOverride, // Opzionale: l'avviso forzato del manager }) async { try { // 1. Inserimento del Task principale -> otteniamo il taskId // 2. Inserimento dei record in task_assignments final String taskId = task.id; List> remindersToInsert = []; // 3. Recuperiamo i default degli ALTRI utenti assegnati final otherStaffIds = assignedStaffIds .where((id) => id != currentUserId) .toList(); List otherDefaults = []; if (otherStaffIds.isNotEmpty) { otherDefaults = await _supabase .from('staff_task_reminder_defaults') .select() .inFilter('staff_id', otherStaffIds); } // 4. CICLO DI COSTRUZIONE DELLA CODA REMINDER for (var staffId in assignedStaffIds) { // CASO A: È l'utente loggato che sta creando/partecipando al task if (staffId == currentUserId) { for (var config in currentUserCustomReminders) { final triggerAt = task.dueDate?.subtract( Duration(minutes: config.minutesBefore), ); if (triggerAt != null && triggerAt.isAfter(DateTime.now())) { remindersToInsert.add({ 'company_id': task.companyId, 'task_id': taskId, 'staff_id': currentUserId, 'minutes_before': config.minutesBefore, 'channel': config.channel, 'trigger_at': triggerAt.toIso8601String(), 'is_forced': false, }); } } } // CASO B: Sono gli altri assegnatari -> ereditano i loro default personali dal DB else { final staffRules = otherDefaults.where( (row) => row['staff_id'] == staffId, ); for (var rule in staffRules) { final minutesBefore = rule['minutes_before'] as int; final triggerAt = task.dueDate?.subtract( Duration(minutes: minutesBefore), ); if (triggerAt != null && triggerAt.isAfter(DateTime.now())) { remindersToInsert.add({ 'company_id': task.companyId, 'task_id': taskId, 'staff_id': staffId, 'minutes_before': minutesBefore, 'channel': rule['channel'], 'trigger_at': triggerAt.toIso8601String(), 'is_forced': false, }); } } } // CASO C: Il creatore ha impostato un avviso forzato (Override molto importante) if (managerForcedOverride != null && task.dueDate != null) { final triggerAt = task.dueDate!.subtract( Duration(minutes: managerForcedOverride.minutesBefore), ); if (triggerAt.isAfter(DateTime.now())) { remindersToInsert.add({ 'company_id': task.companyId, 'task_id': taskId, 'staff_id': staffId, // Lo beccheranno tutti 'minutes_before': managerForcedOverride.minutesBefore, 'channel': managerForcedOverride.channel, 'trigger_at': triggerAt.toIso8601String(), 'is_forced': true, // Chiude la possibilità di cancellarlo lato utente }); } } } // 5. Sparata unica di Bulk Insert su task_reminders if (remindersToInsert.isNotEmpty) { await _supabase.from('task_reminders').insert(remindersToInsert); } } catch (e) { throw Exception('Errore creazione task: $e'); } } // --- 4. ELIMINAZIONE DEL TASK --- Future deleteTask(String taskId) async { try { // Eliminando il task, se la Foreign Key su Supabase ha "ON DELETE CASCADE", // le righe nella tabella di giunzione si distruggeranno da sole in automatico! await _supabase.from(Tables.tasks).delete().eq('id', taskId); } catch (e) { throw Exception('Errore nell\'eliminazione del task: $e'); } } // --- HELPER: RECUPERO SINGOLO TASK --- Future getTaskById(String taskId) async { try { final response = await _supabase .from(Tables.tasks) .select(''' *, task_assignments:${Tables.taskAssignments} ( ${Tables.staffMembers} (*) ) ''') .eq('id', taskId) .single(); return TaskModel.fromMap(response); } catch (e) { throw Exception('Errore nel recupero del singolo task: $e'); } } // --- HELPER: SINCRONIZZAZIONE DELLA TABELLA DI GIUNZIONE --- Future _syncAssignments(String taskId, List staffIds) async { // TECNICA "WIPE & REPLACE": // Invece di capire chi è stato aggiunto e chi tolto, eliminiamo tutti i record // di questo task e li reinseriamo. È fulmineo ed esclude a priori bug di disallineamento. // 1. Facciamo piazza pulita await _supabase.from(Tables.taskAssignments).delete().eq('task_id', taskId); // 2. Inseriamo le nuove assegnazioni (se ce ne sono) if (staffIds.isNotEmpty) { final List> assignments = staffIds .map( (staffId) => { 'task_id': taskId, 'staff_id': staffId, 'company_id': GetIt.I.get().state.company!.id!, }, ) .toList(); await _supabase.from(Tables.taskAssignments).insert(assignments); } } // --- IL MOTORE DELLA MAGIA --- Future generateTaskReminders({ required String taskId, required String companyId, required List assignedStaffIds, required DateTime? taskDueDate, }) async { if (assignedStaffIds.isEmpty) return; try { // 1. Recuperiamo i default di TUTTI i collaboratori coinvolti in un colpo solo final response = await _supabase .from('staff_task_reminder_defaults') .select() .eq('company_id', companyId) .inFilter('staff_id', assignedStaffIds); final List> remindersToInsert = []; for (var staffId in assignedStaffIds) { // Cerchiamo le preferenze di questo specifico membro dello staff final staffDefaults = response.where( (row) => row['staff_id'] == staffId, ); if (staffDefaults.isEmpty) { // STRATEGIA FALLBACK: Se l'utente non ha mai configurato i suoi default, // creiamo un reminder standard (es. una push 15 min prima) per non lasciarlo scoperto. if (taskDueDate != null) { remindersToInsert.add({ 'company_id': companyId, 'task_id': taskId, 'staff_id': staffId, 'minutes_before': 15, 'channel': 'push', 'trigger_at': taskDueDate .subtract(const Duration(minutes: 15)) .toIso8601String(), }); } // E spariamo la push di creazione immediata come comportamento standard _triggerImmediateNotification(staffId, taskId, 'push_creation'); continue; } // 2. GESTIONE NOTIFICHE ISTANTANEE (EVENT-DRIVEN) // Prendiamo la prima riga delle impostazioni dell'utente (tanto le colonne nuove sono speculari) final userSetting = staffDefaults.first; if (userSetting['notify_on_creation_push'] == true) { _triggerImmediateNotification(staffId, taskId, 'push_creation'); } if (userSetting['notify_on_creation_email'] == true) { _triggerImmediateNotification(staffId, taskId, 'email_creation'); } // 3. GESTIONE REMINDER TEMPORIZZATI (TIME-DRIVEN) if (taskDueDate != null) { for (var rule in staffDefaults) { final minutesBefore = rule['minutes_before'] as int; final triggerAt = taskDueDate.subtract( Duration(minutes: minutesBefore), ); // Se il task scade tra 5 minuti e il reminder è impostato a 1 ora prima, // il trigger_at sarebbe nel passato. Lo inseriamo solo se è nel futuro! if (triggerAt.isAfter(DateTime.now())) { remindersToInsert.add({ 'company_id': companyId, 'task_id': taskId, 'staff_id': staffId, 'minutes_before': minutesBefore, 'channel': rule['channel'], 'trigger_at': triggerAt.toIso8601String(), }); } } } } // 4. Bulk Insert dei reminder temporizzati nella coda operativa if (remindersToInsert.isNotEmpty) { await _supabase.from('task_reminders').insert(remindersToInsert); } } catch (e) { debugPrint('Errore nella generazione dei reminder: $e'); } } void _triggerImmediateNotification( String staffId, String taskId, String type, ) { // Questa funzione chiamerà direttamente l'Edge Function di Supabase // per far squillare il telefono o mandare la mail ADESSO. // La implementeremo appena il backend sarà pronto! } }