From 9bace01b9324b2440a8efd15b91c6a6fa05aad9c Mon Sep 17 00:00:00 2001 From: mark-cachy Date: Fri, 29 May 2026 19:24:40 +0200 Subject: [PATCH] b --- lib/core/routes/app_router.dart | 2 +- .../blocs/dashboard_task_list_cubit.dart | 2 +- lib/features/tasks/blocs/task_form_cubit.dart | 138 +++- lib/features/tasks/blocs/task_form_state.dart | 10 +- lib/features/tasks/blocs/task_list_cubit.dart | 2 +- lib/features/tasks/data/task_repository.dart | 649 +++++++----------- lib/features/tasks/models/task_model.dart | 2 + 7 files changed, 375 insertions(+), 430 deletions(-) diff --git a/lib/core/routes/app_router.dart b/lib/core/routes/app_router.dart index daf1abd..43cc005 100644 --- a/lib/core/routes/app_router.dart +++ b/lib/core/routes/app_router.dart @@ -553,7 +553,7 @@ class AppRouter { BlocProvider( create: (context) => TaskFormCubit( globalStaff: allStaffList, - initialTask: task, + existingTask: task, initialTaskId: realTaskId, ), ), diff --git a/lib/features/home/dashboard_task_list/blocs/dashboard_task_list_cubit.dart b/lib/features/home/dashboard_task_list/blocs/dashboard_task_list_cubit.dart index ba0edb6..062497d 100644 --- a/lib/features/home/dashboard_task_list/blocs/dashboard_task_list_cubit.dart +++ b/lib/features/home/dashboard_task_list/blocs/dashboard_task_list_cubit.dart @@ -10,7 +10,7 @@ import 'package:get_it/get_it.dart'; part 'dashboard_task_list_state.dart'; class DashboardTaskListCubit extends Cubit { - final TaskRepository _repository = GetIt.I.get(); + final TasksRepository _repository = GetIt.I.get(); final String staffId; final String companyId; StreamSubscription? _taskSubscription; diff --git a/lib/features/tasks/blocs/task_form_cubit.dart b/lib/features/tasks/blocs/task_form_cubit.dart index 80cf2be..449e340 100644 --- a/lib/features/tasks/blocs/task_form_cubit.dart +++ b/lib/features/tasks/blocs/task_form_cubit.dart @@ -11,40 +11,105 @@ import 'package:get_it/get_it.dart'; part 'task_form_state.dart'; class TaskFormCubit extends Cubit { - final TaskRepository _repository = GetIt.I.get(); + final TasksRepository _repository = GetIt.I.get(); final SettingsRepository _settingsRepository = GetIt.I .get(); final SessionCubit _sessionCubit = GetIt.I.get(); - TaskFormCubit({TaskModel? existingTask}) - : super( - TaskFormState( - id: existingTask?.id, - title: existingTask?.title ?? '', - description: existingTask?.description ?? '', - dueDate: existingTask?.dueDate, - isGlobal: existingTask?.isGlobal ?? false, - selectedStaffIds: existingTask?.assignedToIds ?? [], - ), - ) { - if (existingTask == null) { - _initializeNewTaskReminders(); - } else { - _loadExistingTaskReminders(existingTask.id!); - } + TaskFormCubit({ + String? initialTaskId, // <-- RIPRISTINATO PER DEEP LINK + TaskModel? existingTask, + }) : super(const TaskFormState()) { + // Avviamo l'inizializzazione centralizzata (gestisce sia mem, sia deep link, sia nuovo) + initForm(initialTaskId: initialTaskId, existingTask: existingTask); } String get _companyId => _sessionCubit.state.company!.id!; String get _currentUserId => _sessionCubit.state.currentStaffMember!.id!; + String? get _currentStoreId => _sessionCubit.state.currentStore?.id; - // --- INIT REMINDER NUOVO TASK --- + // --- ARMED INITIALIZATION (Nuovo, Esistente o Deep Link) --- + Future initForm({ + String? initialTaskId, + TaskModel? existingTask, + }) async { + emit(state.copyWith(status: TaskFormStatus.loading)); + try { + TaskModel? task = existingTask; + + // 1. Se arriviamo da Deep Link col solo ID, lo scarichiamo dal DB + if (initialTaskId != null && task == null) { + task = await _repository.fetchTaskById(initialTaskId); + } + + if (task != null) { + // CASO: TASK ESISTENTE (Modifica o Deep Link pronto) + emit( + state.copyWith( + id: task.id, + title: task.title, + description: task.description, + dueDate: task.dueDate, + isGlobal: task.isGlobal, // Sfrutta il tuo getter storeId == null + selectedStaffIds: task.assignedToIds, + ), + ); + await _loadExistingTaskReminders(task.id!); + } else { + // CASO: NUOVO TASK + await _initializeNewTaskReminders(); + } + + // 2. Carichiamo e raggruppiamo il personale (Global o Store) + await _loadAndGroupStaff(); + + // Mandiamo lo status a 'initial' così il FormScreen sincronizza i controller di testo! + emit(state.copyWith(status: TaskFormStatus.initial)); + } catch (e) { + emit( + state.copyWith( + status: TaskFormStatus.failure, + errorMessage: e.toString(), + ), + ); + } + } + + // --- LOGICA GESTIONE STAFF (GLOBAL STAFF / STORE STAFF) --- + Future _loadAndGroupStaff() async { + // Se isGlobal è true, passiamo null come storeId al repo per tirare giù tutta l'azienda + final List staffList = await _repository + .fetchAvailableStaff( + companyId: _companyId, + storeId: state.isGlobal ? null : _currentStoreId, + ); + + // Raggruppamento per nome del negozio (Mappa { "Nome Negozio": [Membri] }) + final Map> grouped = {}; + for (var staff in staffList) { + final storeName = staff.storeName ?? 'Senza Sede'; + grouped.putIfAbsent(storeName, () => []).add(staff); + } + + emit(state.copyWith(groupedAvailableStaff: grouped)); + } + + // Se l'utente switcha su "Globale Aziendale", ricarichiamo lo staff di conseguenza + void toggleGlobalScope(bool g) async { + emit(state.copyWith(isGlobal: g, status: TaskFormStatus.loading)); + await _loadAndGroupStaff(); + emit( + state.copyWith(status: TaskFormStatus.initial), + ); // Ri-notifichiamo la UI + } + + // --- INIT REMINDER --- Future _initializeNewTaskReminders() async { try { final defaults = await _settingsRepository.getMyReminderDefaults( companyId: _companyId, staffId: _currentUserId, ); - final initialReminders = defaults .map( (d) => TaskReminderConfig( @@ -53,10 +118,8 @@ class TaskFormCubit extends Cubit { ), ) .toList(); - emit(state.copyWith(reminders: initialReminders)); } catch (e) { - // Fallback in caso di errore emit( state.copyWith( reminders: const [ @@ -67,10 +130,8 @@ class TaskFormCubit extends Cubit { } } - // --- INIT REMINDER TASK ESISTENTE --- Future _loadExistingTaskReminders(String taskId) async { try { - // Recuperiamo SOLO i reminder non forzati dell'utente loggato per popolare il form final existingConfigs = await _repository.fetchPersonalReminders( taskId: taskId, staffId: _currentUserId, @@ -81,11 +142,10 @@ class TaskFormCubit extends Cubit { } } - // --- UPDATE CAMPI BASE --- + // --- AGGIORNAMENTO CAMPI --- void updateTitle(String t) => emit(state.copyWith(title: t)); void updateDescription(String d) => emit(state.copyWith(description: d)); void updateDueDate(DateTime? d) => emit(state.copyWith(dueDate: d)); - void toggleGlobalScope(bool g) => emit(state.copyWith(isGlobal: g)); void toggleStaffSelection(String staffId) { final updated = List.from(state.selectedStaffIds); @@ -93,7 +153,22 @@ class TaskFormCubit extends Cubit { emit(state.copyWith(selectedStaffIds: updated)); } - // --- GESTIONE REMINDER NEL FORM --- + void toggleStoreSelection(String storeName, bool selectAll) { + final updated = List.from(state.selectedStaffIds); + final storeStaff = state.groupedAvailableStaff[storeName] ?? []; + + for (var staff in storeStaff) { + if (staff.id == null) continue; + if (selectAll) { + if (!updated.contains(staff.id)) updated.add(staff.id!); + } else { + updated.remove(staff.id); + } + } + emit(state.copyWith(selectedStaffIds: updated)); + } + + // --- AZIONI REMINDER --- void addReminderRule(int minutesBefore, String channel) { final updated = List.from(state.reminders); final newConfig = TaskReminderConfig( @@ -114,7 +189,7 @@ class TaskFormCubit extends Cubit { emit(state.copyWith(reminders: updated)); } - // --- SALVATAGGIO FINALE --- + // --- SALVATAGGIO --- Future saveTask() async { if (!state.isFormValid) return; emit(state.copyWith(status: TaskFormStatus.submitting)); @@ -122,18 +197,18 @@ class TaskFormCubit extends Cubit { final taskToSave = TaskModel( id: state.id, companyId: _companyId, - createdBy: _currentUserId, + createdById: _currentUserId, title: state.title.trim(), description: state.description.trim(), dueDate: state.dueDate, - isGlobal: state.isGlobal, + storeId: state.isGlobal + ? null + : _currentStoreId, // Gestione nativa basata sulla tua logica assignedToIds: state.selectedStaffIds, ); try { if (state.id == null) { - // NUOVO TASK -> CREATE - // Qui potresti passare un managerForcedOverride se implementi la UI per quello await _repository.createTask( task: taskToSave, assignedStaffIds: state.selectedStaffIds, @@ -141,7 +216,6 @@ class TaskFormCubit extends Cubit { currentUserCustomReminders: state.reminders, ); } else { - // VECCHIO TASK -> UPDATE await _repository.updateTask( task: taskToSave, assignedStaffIds: state.selectedStaffIds, diff --git a/lib/features/tasks/blocs/task_form_state.dart b/lib/features/tasks/blocs/task_form_state.dart index 9bf13d3..e392503 100644 --- a/lib/features/tasks/blocs/task_form_state.dart +++ b/lib/features/tasks/blocs/task_form_state.dart @@ -10,8 +10,9 @@ class TaskFormState extends Equatable { final DateTime? dueDate; final bool isGlobal; final List selectedStaffIds; - final List - reminders; // I promemoria (solo dell'utente loggato) + final List reminders; + final Map> + groupedAvailableStaff; // <-- RIPRISTINATO final String? errorMessage; const TaskFormState({ @@ -23,6 +24,7 @@ class TaskFormState extends Equatable { this.isGlobal = false, this.selectedStaffIds = const [], this.reminders = const [], + this.groupedAvailableStaff = const {}, this.errorMessage, }); @@ -37,6 +39,7 @@ class TaskFormState extends Equatable { bool? isGlobal, List? selectedStaffIds, List? reminders, + Map>? groupedAvailableStaff, String? errorMessage, }) { return TaskFormState( @@ -48,6 +51,8 @@ class TaskFormState extends Equatable { isGlobal: isGlobal ?? this.isGlobal, selectedStaffIds: selectedStaffIds ?? this.selectedStaffIds, reminders: reminders ?? this.reminders, + groupedAvailableStaff: + groupedAvailableStaff ?? this.groupedAvailableStaff, errorMessage: errorMessage, ); } @@ -62,6 +67,7 @@ class TaskFormState extends Equatable { isGlobal, selectedStaffIds, reminders, + groupedAvailableStaff, errorMessage, ]; } diff --git a/lib/features/tasks/blocs/task_list_cubit.dart b/lib/features/tasks/blocs/task_list_cubit.dart index bb9dd63..32ff455 100644 --- a/lib/features/tasks/blocs/task_list_cubit.dart +++ b/lib/features/tasks/blocs/task_list_cubit.dart @@ -8,7 +8,7 @@ import 'package:get_it/get_it.dart'; part 'task_list_state.dart'; class TaskListCubit extends Cubit { - final TaskRepository _repository = GetIt.I.get(); + final TasksRepository _repository = GetIt.I.get(); final String currentCompanyId; final String? currentStoreId; diff --git a/lib/features/tasks/data/task_repository.dart b/lib/features/tasks/data/task_repository.dart index 9a94c50..7a39a64 100644 --- a/lib/features/tasks/data/task_repository.dart +++ b/lib/features/tasks/data/task_repository.dart @@ -1,7 +1,6 @@ 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'; @@ -10,46 +9,38 @@ 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; +class TasksRepository { + final _supabase = GetIt.I.get(); - TaskRepository({SupabaseClient? supabase}) - : _supabase = supabase ?? Supabase.instance.client; + // ========================================================================= + // LETTURA REMINDER (Per il form in edit) + // ========================================================================= + Future> fetchPersonalReminders({ + required String taskId, + required String staffId, + }) async { + try { + final response = await _supabase + .from('task_reminders') + .select() + .eq('task_id', taskId) + .eq('staff_id', staffId) + .eq( + 'is_forced', + false, + ); // Peschiamo SOLO quelli modificabili dall'utente - // --- 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; + return (response as List) + .map( + (r) => TaskReminderConfig( + minutesBefore: r['minutes_before'], + channel: r['channel'], + ), + ) + .toList(); + } catch (e) { + throw Exception('Errore fetch personal reminders: $e'); + } } // --- RECUPERO DEI TASK FILTRATI --- @@ -107,385 +98,257 @@ class TaskRepository { } } + // ========================================================================= + // REALTIME STREAM (La sentinella per la bacheca) + // ========================================================================= + Stream> watchCompanyTasks(String companyId) { + return _supabase + .from('tasks') + .stream(primaryKey: ['id']) + .eq('company_id', companyId) + .map((listOfMaps) { + return listOfMaps.map((map) => TaskModel.fromMap(map)).toList(); + }); + } + + // ========================================================================= + // CREAZIONE (Insert) + // ========================================================================= Future createTask({ required TaskModel task, required List assignedStaffIds, required String currentUserId, required List currentUserCustomReminders, - TaskReminderConfig? - managerForcedOverride, // Opzionale: l'avviso forzato del manager + TaskReminderConfig? managerForcedOverride, }) 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) + // 1. Inseriamo il Task principale per farci generare l'ID dal DB + final taskResponse = await _supabase + .from('tasks') + .insert(task.toMap()) // Assicurati che toMap() escluda l'id se è null + .select('id') .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. + final String taskId = taskResponse['id']; - // 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, + // 2. Inseriamo le Assegnazioni (tabella task_assignments) + if (assignedStaffIds.isNotEmpty) { + final assignmentsToInsert = assignedStaffIds + .map( + (staffId) => { 'task_id': taskId, 'staff_id': staffId, - 'minutes_before': minutesBefore, - 'channel': rule['channel'], - 'trigger_at': triggerAt.toIso8601String(), - }); + 'company_id': task.companyId, + }, + ) + .toList(); + await _supabase.from('task_assignments').insert(assignmentsToInsert); + } + + // Se non c'è data di scadenza, niente promemoria a tempo + if (task.dueDate == null || assignedStaffIds.isEmpty) return; + + // 3. Setup Reminder: Peschiamo i default degli ALTRI dipendenti coinvolti + 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. Creiamo la lista Bulk Insert per la tabella task_reminders + List> remindersToInsert = []; + + for (var staffId in assignedStaffIds) { + // A) Se è l'utente loggato -> usa i reminder configurati nel form + if (staffId == currentUserId) { + for (var config in currentUserCustomReminders) { + final triggerAt = task.dueDate!.subtract( + Duration(minutes: config.minutesBefore), + ); + if (triggerAt.isAfter(DateTime.now())) { + remindersToInsert.add( + _buildReminderRow( + task, + taskId, + staffId, + config, + triggerAt, + false, + ), + ); } } } + // B) Se è un collega -> eredita i suoi default preimpostati + else { + final staffRules = otherDefaults.where( + (row) => row['staff_id'] == staffId, + ); + for (var rule in staffRules) { + final config = TaskReminderConfig( + minutesBefore: rule['minutes_before'], + channel: rule['channel'], + ); + final triggerAt = task.dueDate!.subtract( + Duration(minutes: config.minutesBefore), + ); + if (triggerAt.isAfter(DateTime.now())) { + remindersToInsert.add( + _buildReminderRow( + task, + taskId, + staffId, + config, + triggerAt, + false, + ), + ); + } + } + } + + // C) Override forzato del manager (per tutti) + if (managerForcedOverride != null) { + final triggerAt = task.dueDate!.subtract( + Duration(minutes: managerForcedOverride.minutesBefore), + ); + if (triggerAt.isAfter(DateTime.now())) { + remindersToInsert.add( + _buildReminderRow( + task, + taskId, + staffId, + managerForcedOverride, + triggerAt, + true, + ), + ); + } + } } - // 4. Bulk Insert dei reminder temporizzati nella coda operativa + // 5. Inserimento massivo finale if (remindersToInsert.isNotEmpty) { await _supabase.from('task_reminders').insert(remindersToInsert); } } catch (e) { - debugPrint('Errore nella generazione dei reminder: $e'); + throw Exception('Errore durante la creazione del task: $e'); } } - void _triggerImmediateNotification( - String staffId, + // ========================================================================= + // AGGIORNAMENTO (Update) + // ========================================================================= + Future updateTask({ + required TaskModel task, + required List assignedStaffIds, + required String currentUserId, + required List currentUserCustomReminders, + }) async { + try { + final taskId = task.id!; + + // 1. Aggiornamento dati Task Base + await _supabase + .from('tasks') + .update({ + 'title': task.title, + 'description': task.description, + 'due_date': task.dueDate?.toIso8601String(), + 'store_id': task.storeId, + 'updated_at': DateTime.now().toIso8601String(), + }) + .eq('id', taskId); + + // 2. Aggiornamento Assegnazioni: eliminiamo le vecchie, inseriamo le nuove + await _supabase.from('task_assignments').delete().eq('task_id', taskId); + if (assignedStaffIds.isNotEmpty) { + final assignmentsToInsert = assignedStaffIds + .map( + (staffId) => { + 'task_id': taskId, + 'staff_id': staffId, + 'company_id': task.companyId, + }, + ) + .toList(); + await _supabase.from('task_assignments').insert(assignmentsToInsert); + } + + // Se non c'è una data, eliminiamo tutti i vecchi promemoria dell'utente loggato per pulizia + if (task.dueDate == null) { + await _supabase + .from('task_reminders') + .delete() + .eq('task_id', taskId) + .eq('staff_id', currentUserId) + .eq('is_forced', false); + return; + } + + // 3. GESTIONE REMINDER: Puliamo SOLO quelli modificabili dall'utente loggato + await _supabase + .from('task_reminders') + .delete() + .eq('task_id', taskId) + .eq('staff_id', currentUserId) + .eq('is_forced', false); // NON tocchiamo quelli forzati dal manager! + + // 4. Inseriamo le nuove configurazioni salvate dal Cubit (solo se è ancora tra gli assegnatari) + if (assignedStaffIds.contains(currentUserId) && + currentUserCustomReminders.isNotEmpty) { + final List> toInsert = []; + + for (var config in currentUserCustomReminders) { + final triggerAt = task.dueDate!.subtract( + Duration(minutes: config.minutesBefore), + ); + if (triggerAt.isAfter(DateTime.now())) { + toInsert.add( + _buildReminderRow( + task, + taskId, + currentUserId, + config, + triggerAt, + false, + ), + ); + } + } + + if (toInsert.isNotEmpty) { + await _supabase.from('task_reminders').insert(toInsert); + } + } + } catch (e) { + throw Exception('Errore durante l\'aggiornamento del task: $e'); + } + } + + // --- HELPER PRIVATO PER LA MAPPA DEL REMINDER --- + Map _buildReminderRow( + TaskModel task, String taskId, - String type, + String staffId, + TaskReminderConfig config, + DateTime triggerAt, + bool isForced, ) { - // 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! + return { + 'company_id': task.companyId, + 'task_id': taskId, + 'staff_id': staffId, + 'minutes_before': config.minutesBefore, + 'channel': config.channel, + 'trigger_at': triggerAt.toIso8601String(), + 'is_forced': isForced, + 'is_sent': false, + }; } } diff --git a/lib/features/tasks/models/task_model.dart b/lib/features/tasks/models/task_model.dart index a0a7f99..f8c4fca 100644 --- a/lib/features/tasks/models/task_model.dart +++ b/lib/features/tasks/models/task_model.dart @@ -29,6 +29,8 @@ class TaskModel extends Equatable { this.storeId, }); + bool get isGlobal => storeId == null; + // --- FACTORY: MODELLO VUOTO (Per le creazioni) --- factory TaskModel.empty({String? companyId, String? createdById}) { return TaskModel(