This commit is contained in:
2026-05-29 19:24:40 +02:00
parent 5ad3e12b1f
commit 9bace01b93
7 changed files with 375 additions and 430 deletions

View File

@@ -553,7 +553,7 @@ class AppRouter {
BlocProvider<TaskFormCubit>( BlocProvider<TaskFormCubit>(
create: (context) => TaskFormCubit( create: (context) => TaskFormCubit(
globalStaff: allStaffList, globalStaff: allStaffList,
initialTask: task, existingTask: task,
initialTaskId: realTaskId, initialTaskId: realTaskId,
), ),
), ),

View File

@@ -10,7 +10,7 @@ import 'package:get_it/get_it.dart';
part 'dashboard_task_list_state.dart'; part 'dashboard_task_list_state.dart';
class DashboardTaskListCubit extends Cubit<DashboardTaskListState> { class DashboardTaskListCubit extends Cubit<DashboardTaskListState> {
final TaskRepository _repository = GetIt.I.get<TaskRepository>(); final TasksRepository _repository = GetIt.I.get<TasksRepository>();
final String staffId; final String staffId;
final String companyId; final String companyId;
StreamSubscription<void>? _taskSubscription; StreamSubscription<void>? _taskSubscription;

View File

@@ -11,40 +11,105 @@ import 'package:get_it/get_it.dart';
part 'task_form_state.dart'; part 'task_form_state.dart';
class TaskFormCubit extends Cubit<TaskFormState> { class TaskFormCubit extends Cubit<TaskFormState> {
final TaskRepository _repository = GetIt.I.get<TaskRepository>(); final TasksRepository _repository = GetIt.I.get<TasksRepository>();
final SettingsRepository _settingsRepository = GetIt.I final SettingsRepository _settingsRepository = GetIt.I
.get<SettingsRepository>(); .get<SettingsRepository>();
final SessionCubit _sessionCubit = GetIt.I.get<SessionCubit>(); final SessionCubit _sessionCubit = GetIt.I.get<SessionCubit>();
TaskFormCubit({TaskModel? existingTask}) TaskFormCubit({
: super( String? initialTaskId, // <-- RIPRISTINATO PER DEEP LINK
TaskFormState( TaskModel? existingTask,
id: existingTask?.id, }) : super(const TaskFormState()) {
title: existingTask?.title ?? '', // Avviamo l'inizializzazione centralizzata (gestisce sia mem, sia deep link, sia nuovo)
description: existingTask?.description ?? '', initForm(initialTaskId: initialTaskId, existingTask: existingTask);
dueDate: existingTask?.dueDate,
isGlobal: existingTask?.isGlobal ?? false,
selectedStaffIds: existingTask?.assignedToIds ?? [],
),
) {
if (existingTask == null) {
_initializeNewTaskReminders();
} else {
_loadExistingTaskReminders(existingTask.id!);
}
} }
String get _companyId => _sessionCubit.state.company!.id!; String get _companyId => _sessionCubit.state.company!.id!;
String get _currentUserId => _sessionCubit.state.currentStaffMember!.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<void> 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<void> _loadAndGroupStaff() async {
// Se isGlobal è true, passiamo null come storeId al repo per tirare giù tutta l'azienda
final List<StaffMemberModel> staffList = await _repository
.fetchAvailableStaff(
companyId: _companyId,
storeId: state.isGlobal ? null : _currentStoreId,
);
// Raggruppamento per nome del negozio (Mappa { "Nome Negozio": [Membri] })
final Map<String, List<StaffMemberModel>> 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<void> _initializeNewTaskReminders() async { Future<void> _initializeNewTaskReminders() async {
try { try {
final defaults = await _settingsRepository.getMyReminderDefaults( final defaults = await _settingsRepository.getMyReminderDefaults(
companyId: _companyId, companyId: _companyId,
staffId: _currentUserId, staffId: _currentUserId,
); );
final initialReminders = defaults final initialReminders = defaults
.map( .map(
(d) => TaskReminderConfig( (d) => TaskReminderConfig(
@@ -53,10 +118,8 @@ class TaskFormCubit extends Cubit<TaskFormState> {
), ),
) )
.toList(); .toList();
emit(state.copyWith(reminders: initialReminders)); emit(state.copyWith(reminders: initialReminders));
} catch (e) { } catch (e) {
// Fallback in caso di errore
emit( emit(
state.copyWith( state.copyWith(
reminders: const [ reminders: const [
@@ -67,10 +130,8 @@ class TaskFormCubit extends Cubit<TaskFormState> {
} }
} }
// --- INIT REMINDER TASK ESISTENTE ---
Future<void> _loadExistingTaskReminders(String taskId) async { Future<void> _loadExistingTaskReminders(String taskId) async {
try { try {
// Recuperiamo SOLO i reminder non forzati dell'utente loggato per popolare il form
final existingConfigs = await _repository.fetchPersonalReminders( final existingConfigs = await _repository.fetchPersonalReminders(
taskId: taskId, taskId: taskId,
staffId: _currentUserId, staffId: _currentUserId,
@@ -81,11 +142,10 @@ class TaskFormCubit extends Cubit<TaskFormState> {
} }
} }
// --- UPDATE CAMPI BASE --- // --- AGGIORNAMENTO CAMPI ---
void updateTitle(String t) => emit(state.copyWith(title: t)); void updateTitle(String t) => emit(state.copyWith(title: t));
void updateDescription(String d) => emit(state.copyWith(description: d)); void updateDescription(String d) => emit(state.copyWith(description: d));
void updateDueDate(DateTime? d) => emit(state.copyWith(dueDate: d)); void updateDueDate(DateTime? d) => emit(state.copyWith(dueDate: d));
void toggleGlobalScope(bool g) => emit(state.copyWith(isGlobal: g));
void toggleStaffSelection(String staffId) { void toggleStaffSelection(String staffId) {
final updated = List<String>.from(state.selectedStaffIds); final updated = List<String>.from(state.selectedStaffIds);
@@ -93,7 +153,22 @@ class TaskFormCubit extends Cubit<TaskFormState> {
emit(state.copyWith(selectedStaffIds: updated)); emit(state.copyWith(selectedStaffIds: updated));
} }
// --- GESTIONE REMINDER NEL FORM --- void toggleStoreSelection(String storeName, bool selectAll) {
final updated = List<String>.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) { void addReminderRule(int minutesBefore, String channel) {
final updated = List<TaskReminderConfig>.from(state.reminders); final updated = List<TaskReminderConfig>.from(state.reminders);
final newConfig = TaskReminderConfig( final newConfig = TaskReminderConfig(
@@ -114,7 +189,7 @@ class TaskFormCubit extends Cubit<TaskFormState> {
emit(state.copyWith(reminders: updated)); emit(state.copyWith(reminders: updated));
} }
// --- SALVATAGGIO FINALE --- // --- SALVATAGGIO ---
Future<void> saveTask() async { Future<void> saveTask() async {
if (!state.isFormValid) return; if (!state.isFormValid) return;
emit(state.copyWith(status: TaskFormStatus.submitting)); emit(state.copyWith(status: TaskFormStatus.submitting));
@@ -122,18 +197,18 @@ class TaskFormCubit extends Cubit<TaskFormState> {
final taskToSave = TaskModel( final taskToSave = TaskModel(
id: state.id, id: state.id,
companyId: _companyId, companyId: _companyId,
createdBy: _currentUserId, createdById: _currentUserId,
title: state.title.trim(), title: state.title.trim(),
description: state.description.trim(), description: state.description.trim(),
dueDate: state.dueDate, dueDate: state.dueDate,
isGlobal: state.isGlobal, storeId: state.isGlobal
? null
: _currentStoreId, // Gestione nativa basata sulla tua logica
assignedToIds: state.selectedStaffIds, assignedToIds: state.selectedStaffIds,
); );
try { try {
if (state.id == null) { if (state.id == null) {
// NUOVO TASK -> CREATE
// Qui potresti passare un managerForcedOverride se implementi la UI per quello
await _repository.createTask( await _repository.createTask(
task: taskToSave, task: taskToSave,
assignedStaffIds: state.selectedStaffIds, assignedStaffIds: state.selectedStaffIds,
@@ -141,7 +216,6 @@ class TaskFormCubit extends Cubit<TaskFormState> {
currentUserCustomReminders: state.reminders, currentUserCustomReminders: state.reminders,
); );
} else { } else {
// VECCHIO TASK -> UPDATE
await _repository.updateTask( await _repository.updateTask(
task: taskToSave, task: taskToSave,
assignedStaffIds: state.selectedStaffIds, assignedStaffIds: state.selectedStaffIds,

View File

@@ -10,8 +10,9 @@ class TaskFormState extends Equatable {
final DateTime? dueDate; final DateTime? dueDate;
final bool isGlobal; final bool isGlobal;
final List<String> selectedStaffIds; final List<String> selectedStaffIds;
final List<TaskReminderConfig> final List<TaskReminderConfig> reminders;
reminders; // I promemoria (solo dell'utente loggato) final Map<String, List<StaffMemberModel>>
groupedAvailableStaff; // <-- RIPRISTINATO
final String? errorMessage; final String? errorMessage;
const TaskFormState({ const TaskFormState({
@@ -23,6 +24,7 @@ class TaskFormState extends Equatable {
this.isGlobal = false, this.isGlobal = false,
this.selectedStaffIds = const [], this.selectedStaffIds = const [],
this.reminders = const [], this.reminders = const [],
this.groupedAvailableStaff = const {},
this.errorMessage, this.errorMessage,
}); });
@@ -37,6 +39,7 @@ class TaskFormState extends Equatable {
bool? isGlobal, bool? isGlobal,
List<String>? selectedStaffIds, List<String>? selectedStaffIds,
List<TaskReminderConfig>? reminders, List<TaskReminderConfig>? reminders,
Map<String, List<StaffMemberModel>>? groupedAvailableStaff,
String? errorMessage, String? errorMessage,
}) { }) {
return TaskFormState( return TaskFormState(
@@ -48,6 +51,8 @@ class TaskFormState extends Equatable {
isGlobal: isGlobal ?? this.isGlobal, isGlobal: isGlobal ?? this.isGlobal,
selectedStaffIds: selectedStaffIds ?? this.selectedStaffIds, selectedStaffIds: selectedStaffIds ?? this.selectedStaffIds,
reminders: reminders ?? this.reminders, reminders: reminders ?? this.reminders,
groupedAvailableStaff:
groupedAvailableStaff ?? this.groupedAvailableStaff,
errorMessage: errorMessage, errorMessage: errorMessage,
); );
} }
@@ -62,6 +67,7 @@ class TaskFormState extends Equatable {
isGlobal, isGlobal,
selectedStaffIds, selectedStaffIds,
reminders, reminders,
groupedAvailableStaff,
errorMessage, errorMessage,
]; ];
} }

View File

@@ -8,7 +8,7 @@ import 'package:get_it/get_it.dart';
part 'task_list_state.dart'; part 'task_list_state.dart';
class TaskListCubit extends Cubit<TaskListState> { class TaskListCubit extends Cubit<TaskListState> {
final TaskRepository _repository = GetIt.I.get<TaskRepository>(); final TasksRepository _repository = GetIt.I.get<TasksRepository>();
final String currentCompanyId; final String currentCompanyId;
final String? currentStoreId; final String? currentStoreId;

View File

@@ -1,7 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/foundation.dart'; 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/core/enums_and_consts/consts.dart';
import 'package:flux/features/tasks/models/task_reminder_config.dart'; import 'package:flux/features/tasks/models/task_reminder_config.dart';
import 'package:flux/features/tasks/models/task_status.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 // Sostituisci con i percorsi corretti di FLUX
import 'package:flux/features/tasks/models/task_model.dart'; import 'package:flux/features/tasks/models/task_model.dart';
class TaskRepository { class TasksRepository {
final SupabaseClient _supabase; final _supabase = GetIt.I.get<SupabaseClient>();
TaskRepository({SupabaseClient? supabase}) // =========================================================================
: _supabase = supabase ?? Supabase.instance.client; // LETTURA REMINDER (Per il form in edit)
// =========================================================================
Future<List<TaskReminderConfig>> 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) --- return (response as List)
Stream<void> watchCompanyTasks(String companyId) { .map(
// Usiamo un broadcast nel caso più bloc volessero ascoltarlo in futuro (r) => TaskReminderConfig(
final controller = StreamController<void>.broadcast(); minutesBefore: r['minutes_before'],
channel: r['channel'],
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(); .toList();
} catch (e) {
// Quando il Cubit smette di ascoltare, puliamo il canale Supabase in automatico throw Exception('Errore fetch personal reminders: $e');
controller.onCancel = () { }
channel.unsubscribe();
controller.close();
};
return controller.stream;
} }
// --- RECUPERO DEI TASK FILTRATI --- // --- RECUPERO DEI TASK FILTRATI ---
@@ -107,385 +98,257 @@ class TaskRepository {
} }
} }
// =========================================================================
// REALTIME STREAM (La sentinella per la bacheca)
// =========================================================================
Stream<List<TaskModel>> 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<void> createTask({ Future<void> createTask({
required TaskModel task, required TaskModel task,
required List<String> assignedStaffIds, required List<String> assignedStaffIds,
required String currentUserId, required String currentUserId,
required List<TaskReminderConfig> currentUserCustomReminders, required List<TaskReminderConfig> currentUserCustomReminders,
TaskReminderConfig? TaskReminderConfig? managerForcedOverride,
managerForcedOverride, // Opzionale: l'avviso forzato del manager
}) async { }) async {
try { try {
// 1. Inserimento del Task principale -> otteniamo il taskId // 1. Inseriamo il Task principale per farci generare l'ID dal DB
// 2. Inserimento dei record in task_assignments final taskResponse = await _supabase
final String taskId = task.id!; .from('tasks')
.insert(task.toMap()) // Assicurati che toMap() escluda l'id se è null
List<Map<String, dynamic>> remindersToInsert = []; .select('id')
// 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(); .single();
return TaskModel.fromMap(response);
} catch (e) {
throw Exception('Errore nel recupero del singolo task: $e');
}
}
// --- HELPER: SINCRONIZZAZIONE DELLA TABELLA DI GIUNZIONE --- final String taskId = taskResponse['id'];
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 // 2. Inseriamo le Assegnazioni (tabella task_assignments)
await _supabase.from(Tables.taskAssignments).delete().eq('task_id', taskId); if (assignedStaffIds.isNotEmpty) {
final assignmentsToInsert = assignedStaffIds
// 2. Inseriamo le nuove assegnazioni (se ce ne sono)
if (staffIds.isNotEmpty) {
final List<Map<String, dynamic>> assignments = staffIds
.map( .map(
(staffId) => { (staffId) => {
'task_id': taskId, 'task_id': taskId,
'staff_id': staffId, 'staff_id': staffId,
'company_id': GetIt.I.get<SessionCubit>().state.company!.id!, 'company_id': task.companyId,
}, },
) )
.toList(); .toList();
await _supabase.from('task_assignments').insert(assignmentsToInsert);
await _supabase.from(Tables.taskAssignments).insert(assignments);
}
} }
// --- IL MOTORE DELLA MAGIA --- // Se non c'è data di scadenza, niente promemoria a tempo
if (task.dueDate == null || assignedStaffIds.isEmpty) return;
Future<void> generateTaskReminders({ // 3. Setup Reminder: Peschiamo i default degli ALTRI dipendenti coinvolti
required String taskId, final otherStaffIds = assignedStaffIds
required String companyId, .where((id) => id != currentUserId)
required List<String> assignedStaffIds, .toList();
required DateTime? taskDueDate, List<dynamic> otherDefaults = [];
}) async { if (otherStaffIds.isNotEmpty) {
if (assignedStaffIds.isEmpty) return; otherDefaults = await _supabase
try {
// 1. Recuperiamo i default di TUTTI i collaboratori coinvolti in un colpo solo
final response = await _supabase
.from('staff_task_reminder_defaults') .from('staff_task_reminder_defaults')
.select() .select()
.eq('company_id', companyId) .inFilter('staff_id', otherStaffIds);
.inFilter('staff_id', assignedStaffIds); }
final List<Map<String, dynamic>> remindersToInsert = []; // 4. Creiamo la lista Bulk Insert per la tabella task_reminders
List<Map<String, dynamic>> remindersToInsert = [];
for (var staffId in assignedStaffIds) { for (var staffId in assignedStaffIds) {
// Cerchiamo le preferenze di questo specifico membro dello staff // A) Se è l'utente loggato -> usa i reminder configurati nel form
final staffDefaults = response.where( 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, (row) => row['staff_id'] == staffId,
); );
for (var rule in staffRules) {
if (staffDefaults.isEmpty) { final config = TaskReminderConfig(
// STRATEGIA FALLBACK: Se l'utente non ha mai configurato i suoi default, minutesBefore: rule['minutes_before'],
// creiamo un reminder standard (es. una push 15 min prima) per non lasciarlo scoperto. channel: rule['channel'],
if (taskDueDate != null) { );
remindersToInsert.add({ final triggerAt = task.dueDate!.subtract(
'company_id': companyId, Duration(minutes: config.minutesBefore),
'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())) { if (triggerAt.isAfter(DateTime.now())) {
remindersToInsert.add({ remindersToInsert.add(
'company_id': companyId, _buildReminderRow(
'task_id': taskId, task,
'staff_id': staffId, taskId,
'minutes_before': minutesBefore, staffId,
'channel': rule['channel'], config,
'trigger_at': triggerAt.toIso8601String(), triggerAt,
}); false,
} ),
);
} }
} }
} }
// 4. Bulk Insert dei reminder temporizzati nella coda operativa // 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,
),
);
}
}
}
// 5. Inserimento massivo finale
if (remindersToInsert.isNotEmpty) { if (remindersToInsert.isNotEmpty) {
await _supabase.from('task_reminders').insert(remindersToInsert); await _supabase.from('task_reminders').insert(remindersToInsert);
} }
} catch (e) { } 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<void> updateTask({
required TaskModel task,
required List<String> assignedStaffIds,
required String currentUserId,
required List<TaskReminderConfig> 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<Map<String, dynamic>> 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<String, dynamic> _buildReminderRow(
TaskModel task,
String taskId, String taskId,
String type, String staffId,
TaskReminderConfig config,
DateTime triggerAt,
bool isForced,
) { ) {
// Questa funzione chiamerà direttamente l'Edge Function di Supabase return {
// per far squillare il telefono o mandare la mail ADESSO. 'company_id': task.companyId,
// La implementeremo appena il backend sarà pronto! 'task_id': taskId,
'staff_id': staffId,
'minutes_before': config.minutesBefore,
'channel': config.channel,
'trigger_at': triggerAt.toIso8601String(),
'is_forced': isForced,
'is_sent': false,
};
} }
} }

View File

@@ -29,6 +29,8 @@ class TaskModel extends Equatable {
this.storeId, this.storeId,
}); });
bool get isGlobal => storeId == null;
// --- FACTORY: MODELLO VUOTO (Per le creazioni) --- // --- FACTORY: MODELLO VUOTO (Per le creazioni) ---
factory TaskModel.empty({String? companyId, String? createdById}) { factory TaskModel.empty({String? companyId, String? createdById}) {
return TaskModel( return TaskModel(