Files
flux/lib/features/tasks/data/task_repository.dart
2026-05-29 12:26:41 +02:00

492 lines
17 KiB
Dart

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<void> watchCompanyTasks(String companyId) {
// Usiamo un broadcast nel caso più bloc volessero ascoltarlo in futuro
final controller = StreamController<void>.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<List<TaskModel>> getTasks({
required String companyId,
String? storeId,
String? staffId,
List<TaskStatus>? 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<void> createTask({
required TaskModel task,
required List<String> assignedStaffIds,
required String currentUserId,
required List<TaskReminderConfig> 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<Map<String, dynamic>> remindersToInsert = [];
// 3. Recuperiamo i default degli ALTRI utenti assegnati
final otherStaffIds = assignedStaffIds
.where((id) => id != currentUserId)
.toList();
List<dynamic> 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<TaskModel> 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<void> createTask({
required TaskModel task,
required List<String> assignedStaffIds,
required String currentUserId,
required List<TaskReminderConfig> 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<Map<String, dynamic>> remindersToInsert = [];
// 3. Recuperiamo i default degli ALTRI utenti assegnati
final otherStaffIds = assignedStaffIds
.where((id) => id != currentUserId)
.toList();
List<dynamic> 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<void> 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<TaskModel> 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<void> _syncAssignments(String taskId, List<String> 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<Map<String, dynamic>> assignments = staffIds
.map(
(staffId) => {
'task_id': taskId,
'staff_id': staffId,
'company_id': GetIt.I.get<SessionCubit>().state.company!.id!,
},
)
.toList();
await _supabase.from(Tables.taskAssignments).insert(assignments);
}
}
// --- IL MOTORE DELLA MAGIA ---
Future<void> generateTaskReminders({
required String taskId,
required String companyId,
required List<String> 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<Map<String, dynamic>> 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!
}
}