diff --git a/lib/core/enums_and_consts/consts.dart b/lib/core/enums_and_consts/consts.dart index 9458082..dad0841 100644 --- a/lib/core/enums_and_consts/consts.dart +++ b/lib/core/enums_and_consts/consts.dart @@ -18,6 +18,7 @@ class Tables { static const String stores = 'stores'; static const String tasks = 'tasks'; static const String taskAssignments = 'task_assignments'; + static const String taskReminders = 'task_reminders'; static const String tickets = 'tickets'; static const String trackings = 'trackings'; } diff --git a/lib/features/home/dashboard_store_operation_list/bloc/dashboard_store_operation_list_cubit.dart b/lib/features/home/dashboard_store_operation_list/bloc/dashboard_store_operation_list_cubit.dart index 266fa54..7cda1a0 100644 --- a/lib/features/home/dashboard_store_operation_list/bloc/dashboard_store_operation_list_cubit.dart +++ b/lib/features/home/dashboard_store_operation_list/bloc/dashboard_store_operation_list_cubit.dart @@ -51,17 +51,17 @@ class DashboardStoreOperationListCubit void _loadOperationsSilently() async { try { - final operations = await _repository.fetchOperations( + final paginatedData = await _repository.fetchPaginatedOperations( companyId: companyId!, storeId: storeId!, - limit: 10, - offset: 0, + page: 1, + itemsPerPage: 20, ); if (isClosed) return; emit( state.copyWith( status: DashboardStoreOperationListStatus.success, - operations: operations, + operations: paginatedData.operations, error: null, ), ); diff --git a/lib/features/operations/data/operations_repository.dart b/lib/features/operations/data/operations_repository.dart index c4e07f2..6895395 100644 --- a/lib/features/operations/data/operations_repository.dart +++ b/lib/features/operations/data/operations_repository.dart @@ -112,7 +112,7 @@ class OperationsRepository { } // --- RECUPERO PAGINATO CON FILTRI E JOIN --- - Future> fetchOperations({ + /* Future> fetchOperations({ required String companyId, String? storeId, String? staffId, @@ -172,9 +172,9 @@ class OperationsRepository { } catch (e) { throw Exception('$e'); } - } + } */ - Stream> watchStoreOperations({ + Stream>> watchStoreOperations({ required String storeId, required int limit, }) { @@ -183,11 +183,7 @@ class OperationsRepository { .stream(primaryKey: ['id']) .eq('store_id', storeId) .order('created_at', ascending: false) - .limit(limit) - .map( - (listOfMaps) => - listOfMaps.map((map) => OperationModel.fromMap(map)).toList(), - ); + .limit(limit); } // --- SALVATAGGIO COMPLETO (PRIMA PADRE, POI FIGLI) --- diff --git a/lib/features/tasks/blocs/task_form_cubit.dart b/lib/features/tasks/blocs/task_form_cubit.dart index 7d79140..e76f6d7 100644 --- a/lib/features/tasks/blocs/task_form_cubit.dart +++ b/lib/features/tasks/blocs/task_form_cubit.dart @@ -8,6 +8,7 @@ import 'package:flux/features/settings/data/settings_repository.dart'; import 'package:flux/features/tasks/data/task_repository.dart'; import 'package:flux/features/tasks/models/task_model.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'; part 'task_form_state.dart'; @@ -30,7 +31,7 @@ class TaskFormCubit extends Cubit { } String get _companyId => _sessionCubit.state.company!.id!; - String get _currentUserId => _sessionCubit.state.currentStaffMember!.id!; + StaffMemberModel get _currentUser => _sessionCubit.state.currentStaffMember!; String? get _currentStoreId => _sessionCubit.state.currentStore?.id; // --- ARMED INITIALIZATION (Nuovo, Esistente o Deep Link) --- @@ -129,7 +130,7 @@ class TaskFormCubit extends Cubit { try { final defaults = await _settingsRepository.getMyReminderDefaults( companyId: _companyId, - staffId: _currentUserId, + staffId: _currentUser.id!, ); final initialReminders = defaults .map( @@ -155,7 +156,7 @@ class TaskFormCubit extends Cubit { try { final existingConfigs = await _repository.fetchPersonalReminders( taskId: taskId, - staffId: _currentUserId, + staffId: _currentUser.id!, ); emit(state.copyWith(reminders: existingConfigs)); } catch (e) { @@ -218,7 +219,7 @@ class TaskFormCubit extends Cubit { final taskToSave = TaskModel( id: state.id, companyId: _companyId, - createdById: _currentUserId, + createdBy: _currentUser, title: state.title.trim(), description: state.description.trim(), dueDate: state.dueDate, @@ -233,14 +234,14 @@ class TaskFormCubit extends Cubit { await _repository.createTask( task: taskToSave, assignedStaffIds: state.selectedStaffIds, - currentUserId: _currentUserId, + currentUserId: _currentUser.id!, currentUserCustomReminders: state.reminders, ); } else { await _repository.updateTask( task: taskToSave, assignedStaffIds: state.selectedStaffIds, - currentUserId: _currentUserId, + currentUserId: _currentUser.id!, currentUserCustomReminders: state.reminders, ); } @@ -254,4 +255,47 @@ class TaskFormCubit extends Cubit { ); } } + + Future deleteTask() async { + if (state.id == null) return; + emit(state.copyWith(status: TaskFormStatus.submitting)); + try { + await _repository.deleteTask(state.id!); + emit(state.copyWith(status: TaskFormStatus.success)); + } catch (e) { + emit( + state.copyWith( + status: TaskFormStatus.failure, + errorMessage: e.toString(), + ), + ); + } + } + + Future updateTaskStatus(TaskStatus newStatus) async { + try { + // Chiamiamo il repo passando il task aggiornato + await _repository.updateTaskStatus( + taskId: state.id!, + newStatus: newStatus, + updatedById: _currentUser.id!, + ); + + if (!isClosed) { + // Se l'update va a buon fine, aggiorniamo lo stato locale del cubit + emit( + state.copyWith(status: TaskFormStatus.success, taskStatus: newStatus), + ); + } + } catch (e) { + if (!isClosed) { + emit( + state.copyWith( + status: TaskFormStatus.failure, + errorMessage: e.toString(), + ), + ); + } + } + } } diff --git a/lib/features/tasks/blocs/task_form_state.dart b/lib/features/tasks/blocs/task_form_state.dart index e392503..f64adcd 100644 --- a/lib/features/tasks/blocs/task_form_state.dart +++ b/lib/features/tasks/blocs/task_form_state.dart @@ -11,9 +11,9 @@ class TaskFormState extends Equatable { final bool isGlobal; final List selectedStaffIds; final List reminders; - final Map> - groupedAvailableStaff; // <-- RIPRISTINATO + final Map> groupedAvailableStaff; final String? errorMessage; + final TaskStatus taskStatus; const TaskFormState({ this.id, @@ -26,6 +26,7 @@ class TaskFormState extends Equatable { this.reminders = const [], this.groupedAvailableStaff = const {}, this.errorMessage, + this.taskStatus = TaskStatus.open, }); bool get isFormValid => title.trim().isNotEmpty; @@ -41,6 +42,7 @@ class TaskFormState extends Equatable { List? reminders, Map>? groupedAvailableStaff, String? errorMessage, + TaskStatus? taskStatus, }) { return TaskFormState( id: id ?? this.id, @@ -54,6 +56,7 @@ class TaskFormState extends Equatable { groupedAvailableStaff: groupedAvailableStaff ?? this.groupedAvailableStaff, errorMessage: errorMessage, + taskStatus: taskStatus ?? this.taskStatus, ); } @@ -69,5 +72,6 @@ class TaskFormState extends Equatable { reminders, groupedAvailableStaff, errorMessage, + taskStatus, ]; } diff --git a/lib/features/tasks/data/task_repository.dart b/lib/features/tasks/data/task_repository.dart index 0989f91..c7df756 100644 --- a/lib/features/tasks/data/task_repository.dart +++ b/lib/features/tasks/data/task_repository.dart @@ -21,7 +21,7 @@ class TasksRepository { }) async { try { final response = await _supabase - .from('task_reminders') + .from(Tables.taskReminders) .select() .eq('task_id', taskId) .eq('staff_id', staffId) @@ -53,13 +53,17 @@ class TasksRepository { int? limit, }) async { try { - // 1. FASE FILTRI: Usa il join esplicito stile "Notes" + // 1. FASE FILTRI: Disambiguazione completa su Tasks e Assignments var filterBuilder = _supabase .from(Tables.tasks) .select(''' *, + creator:${Tables.staffMembers}!created_by_id(*), + updater:${Tables.staffMembers}!updated_by_id(*), task_assignments:${Tables.taskAssignments} ( - ${Tables.staffMembers} (*) + *, + assignee:${Tables.staffMembers}!staff_id(*), + assigner:${Tables.staffMembers}!assigned_by_id(*) ) ''') .eq('company_id', companyId); @@ -71,7 +75,6 @@ class TasksRepository { } if (staffId != null) { - // Grazie al trigger, hai l'array pronto per il filtro senza impazzire! filterBuilder = filterBuilder.contains('assigned_to_ids', [staffId]); } @@ -105,8 +108,12 @@ class TasksRepository { .from(Tables.tasks) .select(''' *, + creator:${Tables.staffMembers}!created_by_id(*), + updater:${Tables.staffMembers}!updated_by_id(*), task_assignments:${Tables.taskAssignments} ( - ${Tables.staffMembers} (*) + *, + assignee:${Tables.staffMembers}!staff_id(*), + assigner:${Tables.staffMembers}!assigned_by_id(*) ) ''') .eq('id', taskId) @@ -122,14 +129,11 @@ class TasksRepository { // ========================================================================= // REALTIME STREAM (La sentinella per la bacheca) // ========================================================================= - Stream> watchCompanyTasks(String companyId) { + 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(); - }); + .eq('company_id', companyId); } // ========================================================================= @@ -160,6 +164,7 @@ class TasksRepository { 'task_id': taskId, 'staff_id': staffId, 'company_id': task.companyId, + 'assigned_by_id': currentUserId, }, ) .toList(); @@ -276,7 +281,7 @@ class TasksRepository { // 1. Aggiornamento dati Task Base await _supabase - .from('tasks') + .from(Tables.tasks) .update({ 'title': task.title, 'description': task.description, @@ -286,15 +291,51 @@ class TasksRepository { }) .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 + // 🥷 2. GESTIONE CHIRURGICA DELLE ASSEGNAZIONI (Addio spam!) + + // A) Recuperiamo chi è GIÀ assegnato a questo task + final existingAssignmentsResponse = await _supabase + .from('task_assignments') + .select('staff_id') + .eq('task_id', taskId); + + final List existingStaffIds = + (existingAssignmentsResponse as List) + .map((row) => row['staff_id'] as String) + .toList(); + + // B) Calcoliamo i Delta con i Set di Dart (Pura magia matematica) + final newStaffIdsSet = assignedStaffIds.toSet(); + final existingStaffIdsSet = existingStaffIds.toSet(); + + // Quelli da inserire (presenti nei nuovi, ma non nei vecchi) + final toInsertIds = newStaffIdsSet + .difference(existingStaffIdsSet) + .toList(); + + // Quelli da eliminare (presenti nei vecchi, ma non nei nuovi) + final toDeleteIds = existingStaffIdsSet + .difference(newStaffIdsSet) + .toList(); + + // C) Eseguiamo solo lo stretto necessario + if (toDeleteIds.isNotEmpty) { + await _supabase + .from('task_assignments') + .delete() + .eq('task_id', taskId) + .inFilter('staff_id', toDeleteIds); + } + + if (toInsertIds.isNotEmpty) { + final assignmentsToInsert = toInsertIds .map( (staffId) => { 'task_id': taskId, 'staff_id': staffId, 'company_id': task.companyId, + 'assigned_by_id': + currentUserId, // Il nostro salvavita anti-fantasma }, ) .toList(); @@ -352,6 +393,35 @@ class TasksRepository { } } + Future updateTaskStatus({ + required String taskId, + required TaskStatus newStatus, + required String? updatedById, + }) async { + try { + await _supabase + .from(Tables.tasks) + .update({ + 'status': newStatus.toValue, + 'updated_by_id': updatedById, + 'updated_at': DateTime.now().toIso8601String(), + }) + .eq('id', taskId); + } catch (e) { + throw Exception( + 'Errore durante l\'aggiornamento dello stato del task: $e', + ); + } + } + + Future deleteTask(String taskId) async { + try { + await _supabase.from(Tables.tasks).delete().eq('id', taskId); + } catch (e) { + throw Exception('Errore durante la cancellazione del task: $e'); + } + } + // --- HELPER PRIVATO PER LA MAPPA DEL REMINDER --- Map _buildReminderRow( TaskModel task, diff --git a/lib/features/tasks/models/task_model.dart b/lib/features/tasks/models/task_model.dart index f8c4fca..ee932a6 100644 --- a/lib/features/tasks/models/task_model.dart +++ b/lib/features/tasks/models/task_model.dart @@ -1,4 +1,5 @@ import 'package:equatable/equatable.dart'; +import 'package:flux/core/utils/extensions.dart'; import 'package:flux/features/master_data/staff/models/staff_member_model.dart'; import 'package:flux/features/tasks/models/task_status.dart'; @@ -9,11 +10,12 @@ class TaskModel extends Equatable { final String? description; final List assignedToIds; final List assignedToStaff; // I dati completi dal JOIN - final String? createdById; + final StaffMemberModel? createdBy; final DateTime? dueDate; final TaskStatus status; final DateTime? createdAt; final String? storeId; + final StaffMemberModel? updatedBy; const TaskModel({ this.id, @@ -22,24 +24,25 @@ class TaskModel extends Equatable { this.description, this.assignedToIds = const [], this.assignedToStaff = const [], - this.createdById, + this.createdBy, this.dueDate, this.status = TaskStatus.open, this.createdAt, this.storeId, + this.updatedBy, }); bool get isGlobal => storeId == null; // --- FACTORY: MODELLO VUOTO (Per le creazioni) --- - factory TaskModel.empty({String? companyId, String? createdById}) { + factory TaskModel.empty({String? companyId, StaffMemberModel? createdBy}) { return TaskModel( companyId: companyId, title: '', description: '', assignedToIds: const [], assignedToStaff: const [], - createdById: createdById, + createdBy: createdBy, status: TaskStatus.open, createdAt: DateTime.now(), ); @@ -54,11 +57,12 @@ class TaskModel extends Equatable { description, assignedToIds, assignedToStaff, - createdById, + createdBy, dueDate, status, createdAt, storeId, + updatedBy, ]; // --- COPY WITH --- @@ -69,13 +73,15 @@ class TaskModel extends Equatable { String? description, List? assignedToIds, List? assignedToStaff, - String? createdById, + StaffMemberModel? createdBy, DateTime? dueDate, bool clearDueDate = false, // Flag ninja per resettare la scadenza TaskStatus? status, DateTime? createdAt, String? storeId, bool clearStoreId = false, + StaffMemberModel? updatedBy, + String? updatedByDisplayName, }) { return TaskModel( id: id ?? this.id, @@ -84,11 +90,12 @@ class TaskModel extends Equatable { description: description ?? this.description, assignedToIds: assignedToIds ?? this.assignedToIds, assignedToStaff: assignedToStaff ?? this.assignedToStaff, - createdById: createdById ?? this.createdById, + createdBy: createdBy ?? this.createdBy, dueDate: clearDueDate ? null : (dueDate ?? this.dueDate), status: status ?? this.status, createdAt: createdAt ?? this.createdAt, storeId: clearStoreId ? null : (storeId ?? this.storeId), + updatedBy: updatedBy ?? this.updatedBy, ); } @@ -124,7 +131,9 @@ class TaskModel extends Equatable { description: map['description'] as String?, assignedToIds: parsedAssignedToIds, assignedToStaff: staffList, - createdById: map['created_by_id'] as String?, + createdBy: map['created_by_id'] != null + ? StaffMemberModel.fromMap(map['creator']) + : null, dueDate: map['due_date'] != null ? DateTime.parse(map['due_date'] as String).toLocal() : null, @@ -133,6 +142,9 @@ class TaskModel extends Equatable { ? DateTime.parse(map['created_at'] as String).toLocal() : null, storeId: map['store_id'] as String?, + updatedBy: map['updated_by_id'] != null + ? StaffMemberModel.fromMap(map['updater']) + : null, ); } @@ -145,10 +157,11 @@ class TaskModel extends Equatable { if (description != null) 'description': description, // Passiamo l'array vuoto se non ci sono assegnazioni 'assigned_to_ids': assignedToIds.isEmpty ? null : assignedToIds, - if (createdById != null) 'created_by_id': createdById, + if (createdBy != null) 'created_by_id': createdBy!.id, 'due_date': dueDate?.toUtc().toIso8601String(), 'status': status.toValue, 'store_id': storeId, + if (updatedBy != null) 'updated_by_id': updatedBy!.id, }; } } diff --git a/lib/features/tasks/ui/task_form_screen.dart b/lib/features/tasks/ui/task_form_screen.dart index 3c5771d..0d6048a 100644 --- a/lib/features/tasks/ui/task_form_screen.dart +++ b/lib/features/tasks/ui/task_form_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/features/tasks/blocs/task_form_cubit.dart'; +import 'package:flux/features/tasks/models/task_status.dart'; import 'package:go_router/go_router.dart'; class TaskFormScreen extends StatefulWidget { @@ -182,6 +183,43 @@ class _TaskFormScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + if (state.id != null && + state.taskStatus != TaskStatus.completed) + Container( + margin: const EdgeInsets.only(bottom: 24.0), + width: double.infinity, + child: ElevatedButton.icon( + onPressed: () { + // Chiama direttamente l'update immediato nel DB! + context + .read() + .updateTaskStatus(TaskStatus.completed); + }, + icon: const Icon( + Icons.check_circle_outline, + size: 28, + ), + label: const Text( + 'Segna come Completato', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green.shade600, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + vertical: 16, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 2, + ), + ), + ), + const Divider(height: 30), _buildFormFields(context, state, cubit), const SizedBox(height: 30), ElevatedButton.icon( diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 1685c88..67cab97 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -1,27 +1,159 @@ PODS: + - app_links (6.4.1): + - FlutterMacOS + - file_picker (0.0.1): + - FlutterMacOS + - file_selector_macos (0.0.1): + - FlutterMacOS + - Firebase/CoreOnly (12.13.0): + - FirebaseCore (~> 12.13.0) + - Firebase/Messaging (12.13.0): + - Firebase/CoreOnly + - FirebaseMessaging (~> 12.13.0) + - firebase_core (4.9.0): + - Firebase/CoreOnly (~> 12.13.0) + - FlutterMacOS + - firebase_messaging (16.2.2): + - Firebase/CoreOnly (~> 12.13.0) + - Firebase/Messaging (~> 12.13.0) + - firebase_core + - FlutterMacOS + - FirebaseCore (12.13.0): + - FirebaseCoreInternal (~> 12.13.0) + - GoogleUtilities/Environment (~> 8.1) + - GoogleUtilities/Logger (~> 8.1) + - FirebaseCoreInternal (12.13.0): + - "GoogleUtilities/NSData+zlib (~> 8.1)" + - FirebaseInstallations (12.13.0): + - FirebaseCore (~> 12.13.0) + - GoogleUtilities/Environment (~> 8.1) + - GoogleUtilities/UserDefaults (~> 8.1) + - PromisesObjC (~> 2.4) + - FirebaseMessaging (12.13.0): + - FirebaseCore (~> 12.13.0) + - FirebaseInstallations (~> 12.13.0) + - GoogleDataTransport (~> 10.1) + - GoogleUtilities/AppDelegateSwizzler (~> 8.1) + - GoogleUtilities/Environment (~> 8.1) + - GoogleUtilities/Reachability (~> 8.1) + - GoogleUtilities/UserDefaults (~> 8.1) + - nanopb (~> 3.30910.0) - FlutterMacOS (1.0.0) + - GoogleDataTransport (10.1.0): + - nanopb (~> 3.30910.0) + - PromisesObjC (~> 2.4) + - GoogleUtilities/AppDelegateSwizzler (8.1.0): + - GoogleUtilities/Environment + - GoogleUtilities/Logger + - GoogleUtilities/Network + - GoogleUtilities/Privacy + - GoogleUtilities/Environment (8.1.0): + - GoogleUtilities/Privacy + - GoogleUtilities/Logger (8.1.0): + - GoogleUtilities/Environment + - GoogleUtilities/Privacy + - GoogleUtilities/Network (8.1.0): + - GoogleUtilities/Logger + - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Privacy + - GoogleUtilities/Reachability + - "GoogleUtilities/NSData+zlib (8.1.0)": + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (8.1.0) + - GoogleUtilities/Reachability (8.1.0): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GoogleUtilities/UserDefaults (8.1.0): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - nanopb (3.30910.0): + - nanopb/decode (= 3.30910.0) + - nanopb/encode (= 3.30910.0) + - nanopb/decode (3.30910.0) + - nanopb/encode (3.30910.0) + - package_info_plus (0.0.1): + - FlutterMacOS - pdfx (1.0.0): - FlutterMacOS - printing (1.0.0): - FlutterMacOS + - PromisesObjC (2.4.0) + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + - url_launcher_macos (0.0.1): + - FlutterMacOS DEPENDENCIES: + - app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`) + - file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`) + - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) + - firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`) + - firebase_messaging (from `Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos`) - FlutterMacOS (from `Flutter/ephemeral`) + - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) - pdfx (from `Flutter/ephemeral/.symlinks/plugins/pdfx/macos`) - printing (from `Flutter/ephemeral/.symlinks/plugins/printing/macos`) + - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) + - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) + +SPEC REPOS: + trunk: + - Firebase + - FirebaseCore + - FirebaseCoreInternal + - FirebaseInstallations + - FirebaseMessaging + - GoogleDataTransport + - GoogleUtilities + - nanopb + - PromisesObjC EXTERNAL SOURCES: + app_links: + :path: Flutter/ephemeral/.symlinks/plugins/app_links/macos + file_picker: + :path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos + file_selector_macos: + :path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos + firebase_core: + :path: Flutter/ephemeral/.symlinks/plugins/firebase_core/macos + firebase_messaging: + :path: Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos FlutterMacOS: :path: Flutter/ephemeral + package_info_plus: + :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos pdfx: :path: Flutter/ephemeral/.symlinks/plugins/pdfx/macos printing: :path: Flutter/ephemeral/.symlinks/plugins/printing/macos + shared_preferences_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin + url_launcher_macos: + :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos SPEC CHECKSUMS: + app_links: 05a6ec2341985eb05e9f97dc63f5837c39895c3f + file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a + file_selector_macos: 9e9e068e90ebee155097d00e89ae91edb2374db7 + Firebase: 7d62445aeabdaea36f7d372f33052fed9a72514f + firebase_core: 8a780c7f989df0ca42dcd55332fc1b203f11f848 + firebase_messaging: 3cb926039fe036f6b92834334d6e23ab33007f1f + FirebaseCore: 58905958aa00a061397a0fd759ae4b55bddb3576 + FirebaseCoreInternal: 37bee58388fc6d183f0ab1b32d69ae44f2cf8aad + FirebaseInstallations: 134bde50e477628ded76070efdb12d515d53f948 + FirebaseMessaging: 30564b85d2f81a96f9d312bd23acf8186ff092ae FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 + GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 + GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 + nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 + package_info_plus: f0052d280d17aa382b932f399edf32507174e870 pdfx: 1e79f57f7a6ce2f4a4c30f21fa54d3dc82441b51 printing: c4cf83c78fd684f9bc318e6aadc18972aa48f617 + PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 + shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb + url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 2a55705..2011583 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -248,6 +248,7 @@ 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, E13C7961A1538309550A7824 /* [CP] Embed Pods Frameworks */, + AC0584CA1EFD6A4D37AEE7BD /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -397,6 +398,23 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; + AC0584CA1EFD6A4D37AEE7BD /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; E13C7961A1538309550A7824 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; diff --git a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 3ed4c94..0000000 --- a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,122 +0,0 @@ -{ - "pins" : [ - { - "identity" : "abseil-cpp-binary", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/abseil-cpp-binary.git", - "state" : { - "revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5", - "version" : "1.2024072200.0" - } - }, - { - "identity" : "app-check", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/app-check.git", - "state" : { - "revision" : "61b85103a1aeed8218f17c794687781505fbbef5", - "version" : "11.2.0" - } - }, - { - "identity" : "firebase-ios-sdk", - "kind" : "remoteSourceControl", - "location" : "https://github.com/firebase/firebase-ios-sdk", - "state" : { - "revision" : "8d5b4189f1f482df8d5c58c9985ea70491ef5382", - "version" : "12.14.0" - } - }, - { - "identity" : "google-ads-on-device-conversion-ios-sdk", - "kind" : "remoteSourceControl", - "location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk", - "state" : { - "revision" : "9bfcc6cf435b2e7c5562c1900b8680c594fa9a64", - "version" : "3.6.0" - } - }, - { - "identity" : "googleappmeasurement", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/GoogleAppMeasurement.git", - "state" : { - "revision" : "219e564a8510e983e675c94f77f7f7c50049f22d", - "version" : "12.14.0" - } - }, - { - "identity" : "googledatatransport", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/GoogleDataTransport.git", - "state" : { - "revision" : "617af071af9aa1d6a091d59a202910ac482128f9", - "version" : "10.1.0" - } - }, - { - "identity" : "googleutilities", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/GoogleUtilities.git", - "state" : { - "revision" : "60da361632d0de02786f709bdc0c4df340f7613e", - "version" : "8.1.0" - } - }, - { - "identity" : "grpc-binary", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/grpc-binary.git", - "state" : { - "revision" : "75b31c842f664a0f46a2e590a570e370249fd8f6", - "version" : "1.69.1" - } - }, - { - "identity" : "gtm-session-fetcher", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/gtm-session-fetcher.git", - "state" : { - "revision" : "c0ac7575d70050c2973ba2318bd5af47f8e8153a", - "version" : "5.3.0" - } - }, - { - "identity" : "interop-ios-for-google-sdks", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/interop-ios-for-google-sdks.git", - "state" : { - "revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe", - "version" : "101.0.0" - } - }, - { - "identity" : "leveldb", - "kind" : "remoteSourceControl", - "location" : "https://github.com/firebase/leveldb.git", - "state" : { - "revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1", - "version" : "1.22.5" - } - }, - { - "identity" : "nanopb", - "kind" : "remoteSourceControl", - "location" : "https://github.com/firebase/nanopb.git", - "state" : { - "revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1", - "version" : "2.30910.0" - } - }, - { - "identity" : "promises", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/promises.git", - "state" : { - "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac", - "version" : "2.4.0" - } - } - ], - "version" : 2 -} diff --git a/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 3ed4c94..0000000 --- a/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,122 +0,0 @@ -{ - "pins" : [ - { - "identity" : "abseil-cpp-binary", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/abseil-cpp-binary.git", - "state" : { - "revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5", - "version" : "1.2024072200.0" - } - }, - { - "identity" : "app-check", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/app-check.git", - "state" : { - "revision" : "61b85103a1aeed8218f17c794687781505fbbef5", - "version" : "11.2.0" - } - }, - { - "identity" : "firebase-ios-sdk", - "kind" : "remoteSourceControl", - "location" : "https://github.com/firebase/firebase-ios-sdk", - "state" : { - "revision" : "8d5b4189f1f482df8d5c58c9985ea70491ef5382", - "version" : "12.14.0" - } - }, - { - "identity" : "google-ads-on-device-conversion-ios-sdk", - "kind" : "remoteSourceControl", - "location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk", - "state" : { - "revision" : "9bfcc6cf435b2e7c5562c1900b8680c594fa9a64", - "version" : "3.6.0" - } - }, - { - "identity" : "googleappmeasurement", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/GoogleAppMeasurement.git", - "state" : { - "revision" : "219e564a8510e983e675c94f77f7f7c50049f22d", - "version" : "12.14.0" - } - }, - { - "identity" : "googledatatransport", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/GoogleDataTransport.git", - "state" : { - "revision" : "617af071af9aa1d6a091d59a202910ac482128f9", - "version" : "10.1.0" - } - }, - { - "identity" : "googleutilities", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/GoogleUtilities.git", - "state" : { - "revision" : "60da361632d0de02786f709bdc0c4df340f7613e", - "version" : "8.1.0" - } - }, - { - "identity" : "grpc-binary", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/grpc-binary.git", - "state" : { - "revision" : "75b31c842f664a0f46a2e590a570e370249fd8f6", - "version" : "1.69.1" - } - }, - { - "identity" : "gtm-session-fetcher", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/gtm-session-fetcher.git", - "state" : { - "revision" : "c0ac7575d70050c2973ba2318bd5af47f8e8153a", - "version" : "5.3.0" - } - }, - { - "identity" : "interop-ios-for-google-sdks", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/interop-ios-for-google-sdks.git", - "state" : { - "revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe", - "version" : "101.0.0" - } - }, - { - "identity" : "leveldb", - "kind" : "remoteSourceControl", - "location" : "https://github.com/firebase/leveldb.git", - "state" : { - "revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1", - "version" : "1.22.5" - } - }, - { - "identity" : "nanopb", - "kind" : "remoteSourceControl", - "location" : "https://github.com/firebase/nanopb.git", - "state" : { - "revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1", - "version" : "2.30910.0" - } - }, - { - "identity" : "promises", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/promises.git", - "state" : { - "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac", - "version" : "2.4.0" - } - } - ], - "version" : 2 -} diff --git a/pubspec.yaml b/pubspec.yaml index 743821a..b6af5c7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -55,6 +55,8 @@ dev_dependencies: flutter_lints: ^6.0.0 flutter: + config: + enable-swift-package-manager: false uses-material-design: true generate: true diff --git a/supabase/functions/instant-notifier/index.ts b/supabase/functions/instant-notifier/index.ts index 37b6b2a..ccdb1e8 100644 --- a/supabase/functions/instant-notifier/index.ts +++ b/supabase/functions/instant-notifier/index.ts @@ -11,173 +11,186 @@ serve(async (req) => { if (req.method === 'OPTIONS') return new Response('ok', { headers: corsHeaders }) try { - const bodyText = await req.text(); - const payload = JSON.parse(bodyText); + const bodyText = await req.text() + const payload = JSON.parse(bodyText) - // Estraggo i dati dal payload standard di Supabase - const tableName = payload.table; - const record = payload.record; + const tableName = payload.table + const eventType = payload.type + const record = payload.record + const old_record = payload.old_record if (!tableName || !record) { - throw new Error("Payload non valido, manca table o record."); + throw new Error("Payload non valido, manca table o record.") } - let event_type = ''; - let target_staff_id = ''; - let title = ''; - let description = ''; - let reference_id = ''; - - // Inizializziamo il client Supabase subito, ci serve per le query const supabaseClient = createClient( Deno.env.get('SUPABASE_URL') ?? '', Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? '' ) - // SMISTAMENTO IN BASE ALLA TABELLA - if (tableName === 'task_assignments') { - event_type = 'task_assigned'; - target_staff_id = record.staff_id; - reference_id = record.task_id; - title = 'Nuovo Task Assegnato'; - - // 1. Peschiamo i dettagli completi del task - const { data: taskData } = await supabaseClient - .from('tasks') - .select('*') - .eq('id', reference_id) - .single(); - - // 2. Peschiamo il nome del creatore - let creatorName = "Admin"; - if (taskData?.created_by_id) { - const { data: creatorData } = await supabaseClient - .from('staff_members') - .select('first_name, last_name') - .eq('id', taskData.created_by_id) - .single(); - - if (creatorData) { - creatorName = `${creatorData.first_name} ${creatorData.last_name}`.trim(); - } + // ========================================================================= + // 🥷 1. IDENTIFICARE ESTRARRE I BERSAGLI (CHI DEVO NOTIFICARE?) + // ========================================================================= + let usersToNotify: string[] = [] + let notificationTitle = '' + let notificationBody = '' + let referenceId = '' + let fluxEventType = '' // 'task_assigned', 'task_completed', etc. + + // SCENARIO A: ASSEGNAZIONE TASK + if (tableName === 'task_assignments' && eventType === 'INSERT') { + const assigneeId = record.staff_id + const assignerId = record.assigned_by_id + referenceId = record.task_id + fluxEventType = 'task_assigned' + + if (assigneeId === assignerId) { + return new Response(JSON.stringify({ message: "Auto-assegnazione ignorata." }), { status: 200, headers: corsHeaders }) } - // 3. Formattiamo la data (se esiste) - let dueDateStr = 'Nessuna scadenza'; + usersToNotify.push(assigneeId) + + // Costruiamo i testi + const { data: taskData } = await supabaseClient.from('tasks').select('title, description, due_date').eq('id', referenceId).single() + const { data: assignerData } = await supabaseClient.from('staff_members').select('first_name, last_name').eq('id', assignerId).single() + + const taskTitle = taskData?.title || 'Senza titolo' + const taskDesc = taskData?.description || 'Nessuna descrizione' + const assignerName = assignerData ? `${assignerData.first_name} ${assignerData.last_name}`.trim() : 'Un collega' + + let dueDateStr = 'Nessuna scadenza' if (taskData?.due_date) { - const d = new Date(taskData.due_date); - dueDateStr = d.toLocaleDateString('it-IT'); + dueDateStr = new Date(taskData.due_date).toLocaleDateString('it-IT') } - // 4. Costruiamo il Body multilinea per Android - const taskTitle = taskData?.title || 'Senza titolo'; - const taskDesc = taskData?.description || 'Nessuna descrizione fornita.'; + notificationTitle = 'Nuovo Task Assegnato' + notificationBody = `${taskTitle}\n\nCreato da: ${assignerName}\nScadenza: ${dueDateStr}\nDettagli: ${taskDesc}` + } + + // SCENARIO B: COMPLETAMENTO TASK + else if (tableName === 'tasks' && eventType === 'UPDATE') { + const justCompleted = record.status === 'completed' && old_record.status !== 'completed'; - description = `${taskTitle}\n\nCreato da: ${creatorName}\nScadenza: ${dueDateStr}\nDettagli: ${taskDesc}`; - } + if (!justCompleted) { + return new Response(JSON.stringify({ message: "Update ignorato (non è un completamento)." }), { status: 200, headers: corsHeaders }) + } - // 1. Leggiamo le preferenze specifiche di questo dipendente - const { data: settings, error: settingsError } = await supabaseClient - .from('staff_notification_settings') - .select('*') - .eq('staff_id', target_staff_id) - .single() + const completerId = record.updated_by_id + referenceId = record.id + fluxEventType = 'task_completed' // Nota: assicurati di avere questa colonna o un fallback nelle preferenze - if (settingsError || !settings) throw new Error('Preferenze utente non trovate') - - // 2. Determiniamo QUALI canali usare in base all'evento e agli switch dell'utente - let sendPush = false - let sendEmail = false - - switch (event_type) { - case 'task_assigned': - sendPush = settings.task_assigned_push - sendEmail = settings.task_assigned_email - break - case 'note_invited': - sendPush = settings.note_invited_push - sendEmail = settings.note_invited_email - break - case 'new_operation': - sendPush = settings.new_operation_push - sendEmail = settings.new_operation_email - break - case 'new_ticket': - sendPush = settings.new_ticket_push - sendEmail = settings.new_ticket_email - break - default: - throw new Error('Tipo evento non riconosciuto') - } - - // Se l'utente ha spento tutto, interrompiamo subito risparmiando risorse - if (!sendPush && !sendEmail) { - return new Response(JSON.stringify({ message: 'L\'utente ha disattivato le notifiche per questo evento.' }), { - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 200 - }) - } - - // Se arriviamo qui, dobbiamo inviare qualcosa. Prepariamo i dati dell'utente. - const { data: staffMember } = await supabaseClient - .from('staff_members') - .select('email, first_name') - .eq('id', target_staff_id) - .single() - - // 3. LOGICA PUSH (FCM) - if (sendPush) { - const firebaseSecret = Deno.env.get('FIREBASE_SERVICE_ACCOUNT'); + const { data: assignments } = await supabaseClient.from('task_assignments').select('staff_id').eq('task_id', referenceId) - if (!firebaseSecret) { - console.error("ERRORE: Secret FIREBASE_SERVICE_ACCOUNT mancante nel progetto!"); - } else { - const credentials = JSON.parse(firebaseSecret); - const jwtClient = new JWT({ - email: credentials.client_email, - key: credentials.private_key, - scopes: ['https://www.googleapis.com/auth/firebase.messaging'], - }); - const fcmAccessToken = (await jwtClient.getAccessToken()).token; + if (assignments && assignments.length > 0) { + usersToNotify = assignments.map(a => a.staff_id).filter(id => id !== completerId) + } - const { data: devices } = await supabaseClient - .from('staff_devices') - .select('fcm_token') - .eq('staff_id', target_staff_id); + if (usersToNotify.length === 0) { + return new Response(JSON.stringify({ message: "Nessun altro assegnatario da notificare per la chiusura." }), { status: 200, headers: corsHeaders }) + } - if (devices && devices.length > 0) { + const { data: completerData } = await supabaseClient.from('staff_members').select('first_name, last_name').eq('id', completerId).single() + const completerName = completerData ? `${completerData.first_name} ${completerData.last_name}`.trim() : 'Un collega' + const taskTitle = record.title || 'Senza titolo' + + notificationTitle = 'Task Completato ✅' + notificationBody = `${completerName} ha appena chiuso il task: ${taskTitle}` + } + + // SCENARIO C: ALTRI EVENTI (Es. note_invited, ecc. Mettili qui quando ti serviranno) + else { + return new Response(JSON.stringify({ message: "Tabella o evento non gestito." }), { status: 200, headers: corsHeaders }) + } + + + // ========================================================================= + // 🥷 2. MOTORE DI INVIO MASSIVO PER I BERSAGLI IDENTIFICATI + // ========================================================================= + + // Inizializziamo FCM una volta sola per risparmiare tempo se ci sono push da mandare + const firebaseSecret = Deno.env.get('FIREBASE_SERVICE_ACCOUNT') + let fcmAccessToken = '' + let fcmProjectId = '' + + if (firebaseSecret) { + const credentials = JSON.parse(firebaseSecret) + fcmProjectId = credentials.project_id + const jwtClient = new JWT({ + email: credentials.client_email, + key: credentials.private_key, + scopes: ['https://www.googleapis.com/auth/firebase.messaging'], + }) + fcmAccessToken = (await jwtClient.getAccessToken()).token ?? '' + } + + const resendApiKey = Deno.env.get('RESEND_API_KEY') + + let pushSentCount = 0; + let emailSentCount = 0; + + // Cicliamo tutti gli utenti che meritano la notifica + for (const targetStaffId of usersToNotify) { + + // A) Leggiamo le preferenze dello specifico utente + const { data: settings } = await supabaseClient + .from('staff_notification_settings') + .select('*') + .eq('staff_id', targetStaffId) + .single() + + if (!settings) continue; // Salta se non ha le preferenze + + let sendPush = false + let sendEmail = false + + // B) Traduciamo l'evento nelle sue preferenze + // (Se aggiungi 'task_completed' al DB settings, mettilo qui. Per ora riuso le preesistenti se manca) + switch (fluxEventType) { + case 'task_assigned': + sendPush = settings.task_assigned_push + sendEmail = settings.task_assigned_email + break + case 'task_completed': + // Se nel DB hai aggiunto task_completed_push usa quello. + // Altrimenti puoi fare fallback su task_assigned_push per testare. + sendPush = settings.task_assigned_push + sendEmail = settings.task_assigned_email + break + // Aggiungi qui gli altri case (note_invited, new_operation) + } + + if (!sendPush && !sendEmail) continue; // Questo utente non vuole essere disturbato + + // Recuperiamo nome ed email per questo specifico utente + const { data: staffMember } = await supabaseClient.from('staff_members').select('email, first_name').eq('id', targetStaffId).single() + + // C) INVIO PUSH (FCM) + if (sendPush && fcmAccessToken) { + const { data: devices } = await supabaseClient.from('staff_devices').select('fcm_token').eq('staff_id', targetStaffId) + + if (devices) { for (const device of devices) { try { - const res = await fetch(`https://fcm.googleapis.com/v1/projects/${credentials.project_id}/messages:send`, { + const res = await fetch(`https://fcm.googleapis.com/v1/projects/${fcmProjectId}/messages:send`, { method: 'POST', headers: { 'Authorization': `Bearer ${fcmAccessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ message: { token: device.fcm_token, - notification: { title, body: description }, - data: { click_action: 'FLUTTER_NOTIFICATION_CLICK', eventType: event_type, referenceId: reference_id }, + notification: { title: notificationTitle, body: notificationBody }, + data: { click_action: 'FLUTTER_NOTIFICATION_CLICK', eventType: fluxEventType, referenceId: referenceId }, }, }), }); - - // QUI È DOVE CATTURIAMO LA RISPOSTA DI GOOGLE - const fcmResponseData = await res.json(); - - if (!res.ok) { - console.error("FCM HA RIFIUTATO LA NOTIFICA:", fcmResponseData); - } - - } catch (err) { - console.error('Errore di rete durante invio Push:', err); - } + if (res.ok) pushSentCount++; + else console.error("FCM HA RIFIUTATO LA NOTIFICA:", await res.json()); + } catch (err) { console.error('Errore rete FCM:', err) } } } } - } - // 4. LOGICA EMAIL (Resend) - if (sendEmail && staffMember?.email) { - const resendApiKey = Deno.env.get('RESEND_API_KEY') - if (resendApiKey) { + // D) INVIO EMAIL (Resend) + if (sendEmail && resendApiKey && staffMember?.email) { try { await fetch('https://api.resend.com/emails', { method: 'POST', @@ -185,20 +198,24 @@ serve(async (req) => { body: JSON.stringify({ from: 'FLUX Notifiche ', to: staffMember.email, - subject: title, - html: `

Ciao ${staffMember.first_name},

${description}


Il team FLUX

`, + subject: notificationTitle, + html: `

Ciao ${staffMember.first_name},

${notificationBody.replace(/\n/g, '
')}


Il team FLUX

`, }), }) + emailSentCount++; } catch (err) { console.error('Errore invio Email:', err) } } } - return new Response(JSON.stringify({ success: true, push_sent: sendPush, email_sent: sendEmail }), { - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 200 - }) + return new Response(JSON.stringify({ + success: true, + targets_found: usersToNotify.length, + push_sent: pushSentCount, + email_sent: emailSentCount + }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 200 }) } catch (error) { - console.error("ERRORE FATALE NELLA FUNZIONE:", error); + console.error("ERRORE FATALE NELLA FUNZIONE:", error) return new Response(JSON.stringify({ error: error.message }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 500 })