fcm
This commit is contained in:
@@ -1,9 +1,14 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flux/core/data/core_repository.dart';
|
||||
import 'package:flux/features/company/models/company_model.dart';
|
||||
import 'package:flux/features/master_data/staff/models/staff_member_model.dart';
|
||||
import 'package:flux/features/master_data/store/models/store_model.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:collection/collection.dart'; // Per firstWhereOrNull
|
||||
@@ -134,6 +139,11 @@ class SessionCubit extends Cubit<SessionState> {
|
||||
onboardingStep: OnboardingStep.none, // Svuotiamo l'onboarding
|
||||
),
|
||||
);
|
||||
// --- REGISTRAZIONE DISPOSITIVO PER NOTIFICHE PUSH ---
|
||||
// Lo chiamiamo SENZA 'await' in modo che il caricamento dell'app non si blocchi.
|
||||
// L'utente entrerà subito nell'app e poi vedrà comparire il popup di sistema
|
||||
// per accettare i permessi delle notifiche.
|
||||
_registerFcmToken(companyId: company.id!, staffId: staff.id!);
|
||||
} catch (e) {
|
||||
// Se esplode il database, non lasciamo l'app freezata in 'initial'
|
||||
emit(
|
||||
@@ -145,6 +155,68 @@ class SessionCubit extends Cubit<SessionState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _registerFcmToken({
|
||||
required String companyId,
|
||||
required String staffId,
|
||||
}) async {
|
||||
// Scudo anti-crash per lo sviluppo su Linux / Windows
|
||||
if (!kIsWeb &&
|
||||
!Platform.isAndroid &&
|
||||
!Platform.isIOS &&
|
||||
!Platform.isMacOS) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final messaging = FirebaseMessaging.instance;
|
||||
|
||||
// 1. Richiesta permessi di notifica
|
||||
final settings = await messaging.requestPermission(
|
||||
alert: true,
|
||||
badge: true,
|
||||
sound: true,
|
||||
);
|
||||
|
||||
if (settings.authorizationStatus == AuthorizationStatus.authorized) {
|
||||
// 2. Cattura del token dal device
|
||||
final String? fcmToken = await messaging.getToken();
|
||||
|
||||
if (fcmToken != null) {
|
||||
final supabase = GetIt.I.get<SupabaseClient>();
|
||||
|
||||
// Determiniamo la piattaforma in modo sicuro per Linux
|
||||
String osPlatform = 'web';
|
||||
if (!kIsWeb) {
|
||||
if (Platform.isAndroid) osPlatform = 'android';
|
||||
if (Platform.isIOS) osPlatform = 'ios';
|
||||
if (Platform.isMacOS) osPlatform = 'macos';
|
||||
}
|
||||
|
||||
// 3. UPSERT su Supabase condizionato dal vincolo 'fcm_token'
|
||||
await supabase.from('staff_devices').upsert(
|
||||
{
|
||||
'company_id': companyId,
|
||||
'staff_id': staffId,
|
||||
'fcm_token': fcmToken,
|
||||
'os_platform': osPlatform,
|
||||
'updated_at': DateTime.now().toIso8601String(),
|
||||
},
|
||||
onConflict:
|
||||
'fcm_token', // Se il token esiste già, aggiorna questa riga!
|
||||
);
|
||||
|
||||
debugPrint(
|
||||
'Dispositivo registrato con successo su FLUX Cloud. Platform: $osPlatform',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
debugPrint('Permesso push negato dall\'utente.');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Errore durante la registrazione del dispositivo: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void updateCurrentCompany(CompanyModel newCompany) {
|
||||
emit(state.copyWith(company: newCompany));
|
||||
}
|
||||
|
||||
@@ -545,16 +545,21 @@ class AppRouter {
|
||||
realTaskId = pathId;
|
||||
}
|
||||
|
||||
final allStaffList = context.read<StaffCubit>().state.allStaff;
|
||||
List<StaffMemberModel>? preloadedStaff;
|
||||
try {
|
||||
preloadedStaff = context.read<StaffCubit>().state.allStaff;
|
||||
} catch (_) {
|
||||
preloadedStaff = null; // Fallback se la rotta è isolata
|
||||
}
|
||||
|
||||
// Creiamo il BLoC "al volo" solo per questa schermata
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<TaskFormCubit>(
|
||||
create: (context) => TaskFormCubit(
|
||||
globalStaff: allStaffList,
|
||||
existingTask: task,
|
||||
initialTaskId: realTaskId,
|
||||
allStaff: preloadedStaff,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -232,7 +232,7 @@ class HomeScreen extends StatelessWidget {
|
||||
QuickActionButton(
|
||||
icon: Icons.task_alt,
|
||||
label: context.l10n.commonTask,
|
||||
color: Colors.teal,
|
||||
color: Colors.orange,
|
||||
onTap: () {
|
||||
context.pushNamed(Routes.taskForm, pathParameters: {'id': 'new'});
|
||||
},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flux/core/enums_and_consts/consts.dart';
|
||||
import 'package:flux/features/master_data/staff/models/staff_member_model.dart';
|
||||
import 'package:flux/features/master_data/store/models/store_model.dart';
|
||||
@@ -10,19 +11,38 @@ class StaffRepository {
|
||||
// --- ANAGRAFICA PURA ---
|
||||
|
||||
// Prende tutto lo staff della Company (per l'Hub Anagrafiche)
|
||||
Future<List<StaffMemberModel>> getStaffMembers(String companyId) async {
|
||||
final response = await _supabase
|
||||
.from(Tables.staffMembers)
|
||||
.select('''
|
||||
*,
|
||||
store_assignments:${Tables.staffInStores} (
|
||||
${Tables.stores}(*)
|
||||
)
|
||||
''')
|
||||
.eq('company_id', companyId)
|
||||
.order('name', ascending: true);
|
||||
Future<List<StaffMemberModel>> getStaffMembers(
|
||||
String companyId, {
|
||||
String? storeId,
|
||||
}) async {
|
||||
try {
|
||||
var filterBuilder = _supabase
|
||||
.from(Tables.staffMembers)
|
||||
.select('''
|
||||
*,
|
||||
store_assignments:${Tables.staffInStores} (
|
||||
${Tables.stores}(*)
|
||||
)
|
||||
''')
|
||||
.eq('company_id', companyId);
|
||||
|
||||
return (response as List).map((s) => StaffMemberModel.fromMap(s)).toList();
|
||||
if (storeId != null) {
|
||||
filterBuilder = filterBuilder.or(
|
||||
'store_id.eq.$storeId,store_id.is.null',
|
||||
);
|
||||
}
|
||||
|
||||
var transformBuilder = filterBuilder.order('name', ascending: true);
|
||||
|
||||
final response = await transformBuilder;
|
||||
|
||||
return (response as List)
|
||||
.map((s) => StaffMemberModel.fromMap(s))
|
||||
.toList();
|
||||
} on Exception catch (e) {
|
||||
debugPrint('Errore nel recupero della lista di staff: $e');
|
||||
throw Exception('Errore nel recupero della lista di staff: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<StaffMemberModel?> getStaffMemberById(String staffId) async {
|
||||
|
||||
@@ -18,6 +18,7 @@ class _ReminderSettingsScreenState extends State<ReminderSettingsScreen> {
|
||||
}
|
||||
|
||||
void _showAddReminderBottomSheet(BuildContext context) {
|
||||
final cubit = context.read<ReminderDefaultsCubit>();
|
||||
// Valori preselezionati
|
||||
int selectedMinutes = 15;
|
||||
String selectedChannel = 'push';
|
||||
@@ -73,8 +74,9 @@ class _ReminderSettingsScreenState extends State<ReminderSettingsScreen> {
|
||||
),
|
||||
],
|
||||
onChanged: (val) {
|
||||
if (val != null)
|
||||
if (val != null) {
|
||||
setModalState(() => selectedMinutes = val);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@@ -113,8 +115,9 @@ class _ReminderSettingsScreenState extends State<ReminderSettingsScreen> {
|
||||
),
|
||||
],
|
||||
onChanged: (val) {
|
||||
if (val != null)
|
||||
if (val != null) {
|
||||
setModalState(() => selectedChannel = val);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
@@ -125,7 +128,7 @@ class _ReminderSettingsScreenState extends State<ReminderSettingsScreen> {
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
onPressed: () {
|
||||
context.read<ReminderDefaultsCubit>().addReminder(
|
||||
cubit.addReminder(
|
||||
minutesBefore: selectedMinutes,
|
||||
channel: selectedChannel,
|
||||
);
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flux/core/blocs/session/session_cubit.dart';
|
||||
import 'package:flux/features/master_data/staff/data/staff_repository.dart';
|
||||
import 'package:flux/features/master_data/staff/models/staff_member_model.dart';
|
||||
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';
|
||||
|
||||
@@ -14,12 +15,16 @@ class TaskFormCubit extends Cubit<TaskFormState> {
|
||||
final TasksRepository _repository = GetIt.I.get<TasksRepository>();
|
||||
final SettingsRepository _settingsRepository = GetIt.I
|
||||
.get<SettingsRepository>();
|
||||
final _staffRepository = GetIt.I.get<StaffRepository>();
|
||||
final SessionCubit _sessionCubit = GetIt.I.get<SessionCubit>();
|
||||
final List<StaffMemberModel>? _preloadedStaff;
|
||||
|
||||
TaskFormCubit({
|
||||
String? initialTaskId, // <-- RIPRISTINATO PER DEEP LINK
|
||||
TaskModel? existingTask,
|
||||
}) : super(const TaskFormState()) {
|
||||
List<StaffMemberModel>? allStaff,
|
||||
}) : _preloadedStaff = allStaff,
|
||||
super(const TaskFormState()) {
|
||||
// Avviamo l'inizializzazione centralizzata (gestisce sia mem, sia deep link, sia nuovo)
|
||||
initForm(initialTaskId: initialTaskId, existingTask: existingTask);
|
||||
}
|
||||
@@ -77,18 +82,34 @@ class TaskFormCubit extends Cubit<TaskFormState> {
|
||||
|
||||
// --- 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,
|
||||
);
|
||||
final List<StaffMemberModel> staffList;
|
||||
|
||||
// SE C'È LO STAFF PASCIUTO DALL'APP USA QUELLO, ALTRIMENTI CHIAMA IL REPO
|
||||
if (_preloadedStaff != null && _preloadedStaff.isNotEmpty) {
|
||||
staffList = _preloadedStaff;
|
||||
} else {
|
||||
staffList = await _staffRepository.getStaffMembers(_companyId);
|
||||
}
|
||||
|
||||
// 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);
|
||||
if (!state.isGlobal) {
|
||||
final belongsToCurrentStore = staff.assignedStores.any(
|
||||
(store) => store.id == _currentStoreId,
|
||||
);
|
||||
if (!belongsToCurrentStore) continue;
|
||||
}
|
||||
|
||||
if (staff.assignedStores.isEmpty) {
|
||||
grouped.putIfAbsent('Direzione / Senza Sede', () => []).add(staff);
|
||||
} else {
|
||||
for (var store in staff.assignedStores) {
|
||||
if (!state.isGlobal && store.id != _currentStoreId) continue;
|
||||
final storeName = store.name;
|
||||
grouped.putIfAbsent(storeName, () => []).add(staff);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emit(state.copyWith(groupedAvailableStaff: grouped));
|
||||
@@ -138,7 +159,7 @@ class TaskFormCubit extends Cubit<TaskFormState> {
|
||||
);
|
||||
emit(state.copyWith(reminders: existingConfigs));
|
||||
} catch (e) {
|
||||
print('Errore caricamento reminder: $e');
|
||||
debugPrint('Errore caricamento reminder: $e');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ class TasksRepository {
|
||||
)
|
||||
.toList();
|
||||
} catch (e) {
|
||||
debugPrint('Errore fetch personal reminders: $e');
|
||||
throw Exception('Errore fetch personal reminders: $e');
|
||||
}
|
||||
}
|
||||
@@ -98,6 +99,26 @@ class TasksRepository {
|
||||
}
|
||||
}
|
||||
|
||||
Future<TaskModel?> fetchTaskById(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) {
|
||||
debugPrint('Errore fetch task by id: $e');
|
||||
throw Exception('Errore fetch task by id: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// REALTIME STREAM (La sentinella per la bacheca)
|
||||
// =========================================================================
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flux/core/blocs/session/session_cubit.dart';
|
||||
import 'package:flux/features/tasks/blocs/task_form_cubit.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class TaskFormScreen extends StatefulWidget {
|
||||
@@ -136,15 +134,7 @@ class _TaskFormScreenState extends State<TaskFormScreen> {
|
||||
)
|
||||
else
|
||||
TextButton.icon(
|
||||
onPressed: state.isFormValid
|
||||
? () => cubit.saveTask(
|
||||
currentUserId: GetIt.I
|
||||
.get<SessionCubit>()
|
||||
.state
|
||||
.currentStaffMember!
|
||||
.id!,
|
||||
)
|
||||
: null,
|
||||
onPressed: state.isFormValid ? () => cubit.saveTask() : null,
|
||||
icon: const Icon(Icons.save),
|
||||
label: const Text('Salva'),
|
||||
style: TextButton.styleFrom(
|
||||
|
||||
89
lib/firebase_options.dart
Normal file
89
lib/firebase_options.dart
Normal file
@@ -0,0 +1,89 @@
|
||||
// File generated by FlutterFire CLI.
|
||||
// ignore_for_file: type=lint
|
||||
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
|
||||
import 'package:flutter/foundation.dart'
|
||||
show defaultTargetPlatform, kIsWeb, TargetPlatform;
|
||||
|
||||
/// Default [FirebaseOptions] for use with your Firebase apps.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// import 'firebase_options.dart';
|
||||
/// // ...
|
||||
/// await Firebase.initializeApp(
|
||||
/// options: DefaultFirebaseOptions.currentPlatform,
|
||||
/// );
|
||||
/// ```
|
||||
class DefaultFirebaseOptions {
|
||||
static FirebaseOptions get currentPlatform {
|
||||
if (kIsWeb) {
|
||||
return web;
|
||||
}
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.android:
|
||||
return android;
|
||||
case TargetPlatform.iOS:
|
||||
return ios;
|
||||
case TargetPlatform.macOS:
|
||||
return macos;
|
||||
case TargetPlatform.windows:
|
||||
return windows;
|
||||
case TargetPlatform.linux:
|
||||
throw UnsupportedError(
|
||||
'DefaultFirebaseOptions have not been configured for linux - '
|
||||
'you can reconfigure this by running the FlutterFire CLI again.',
|
||||
);
|
||||
default:
|
||||
throw UnsupportedError(
|
||||
'DefaultFirebaseOptions are not supported for this platform.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static const FirebaseOptions web = FirebaseOptions(
|
||||
apiKey: 'AIzaSyACOLz2mY8fHd5RWfJmDvN53LCd5_TxI6o',
|
||||
appId: '1:249756116297:web:7c652e51004414b7cf2698',
|
||||
messagingSenderId: '249756116297',
|
||||
projectId: 'flux-87e49',
|
||||
authDomain: 'flux-87e49.firebaseapp.com',
|
||||
storageBucket: 'flux-87e49.firebasestorage.app',
|
||||
measurementId: 'G-6V4VN8GWWZ',
|
||||
);
|
||||
|
||||
static const FirebaseOptions android = FirebaseOptions(
|
||||
apiKey: 'AIzaSyA6-uX6504B3yofeo7YQwfQaS0cCDoZnvY',
|
||||
appId: '1:249756116297:android:a2c3d37323752069cf2698',
|
||||
messagingSenderId: '249756116297',
|
||||
projectId: 'flux-87e49',
|
||||
storageBucket: 'flux-87e49.firebasestorage.app',
|
||||
);
|
||||
|
||||
static const FirebaseOptions ios = FirebaseOptions(
|
||||
apiKey: 'AIzaSyAllwaoNyqHsZtqfMMo9DxVS6-q7yBwWow',
|
||||
appId: '1:249756116297:ios:fe9dadca7150da16cf2698',
|
||||
messagingSenderId: '249756116297',
|
||||
projectId: 'flux-87e49',
|
||||
storageBucket: 'flux-87e49.firebasestorage.app',
|
||||
iosBundleId: 'com.catellisrl.flux',
|
||||
);
|
||||
|
||||
static const FirebaseOptions macos = FirebaseOptions(
|
||||
apiKey: 'AIzaSyAllwaoNyqHsZtqfMMo9DxVS6-q7yBwWow',
|
||||
appId: '1:249756116297:ios:fe9dadca7150da16cf2698',
|
||||
messagingSenderId: '249756116297',
|
||||
projectId: 'flux-87e49',
|
||||
storageBucket: 'flux-87e49.firebasestorage.app',
|
||||
iosBundleId: 'com.catellisrl.flux',
|
||||
);
|
||||
|
||||
static const FirebaseOptions windows = FirebaseOptions(
|
||||
apiKey: 'AIzaSyACOLz2mY8fHd5RWfJmDvN53LCd5_TxI6o',
|
||||
appId: '1:249756116297:web:b094277c2fedb425cf2698',
|
||||
messagingSenderId: '249756116297',
|
||||
projectId: 'flux-87e49',
|
||||
authDomain: 'flux-87e49.firebaseapp.com',
|
||||
storageBucket: 'flux-87e49.firebasestorage.app',
|
||||
measurementId: 'G-8E29KT6RWX',
|
||||
);
|
||||
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -22,6 +23,7 @@ import 'package:flux/features/tickets/blocs/ticket_list_cubit.dart';
|
||||
import 'package:flux/features/tickets/data/ticket_repository.dart';
|
||||
import 'package:flux/features/tracking/blocs/tracking_cubit.dart';
|
||||
import 'package:flux/features/tracking/data/tracking_repository.dart';
|
||||
import 'package:flux/firebase_options.dart';
|
||||
import 'package:flux/l10n/app_localizations.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@@ -53,6 +55,16 @@ void main() async {
|
||||
await setupLocator();
|
||||
// RIMUOVE IL CARATTERE # DAGLI URL WEB!
|
||||
usePathUrlStrategy();
|
||||
// Lo Scudo Ninja: Inizializziamo Firebase SOLO sulle piattaforme supportate
|
||||
if (kIsWeb || Platform.isAndroid || Platform.isIOS || Platform.isMacOS) {
|
||||
try {
|
||||
await Firebase.initializeApp(
|
||||
options: DefaultFirebaseOptions.currentPlatform,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('Errore inizializzazione Firebase: $e');
|
||||
}
|
||||
}
|
||||
runApp(
|
||||
MultiBlocProvider(
|
||||
providers: [
|
||||
@@ -138,7 +150,7 @@ Future<void> setupLocator() async {
|
||||
() => TicketsShippingRepository(),
|
||||
);
|
||||
getIt.registerLazySingleton<NotesRepository>(() => NotesRepository());
|
||||
getIt.registerLazySingleton<TaskRepository>(() => TaskRepository());
|
||||
getIt.registerLazySingleton<TasksRepository>(() => TasksRepository());
|
||||
getIt.registerLazySingleton<SettingsRepository>(() => SettingsRepository());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user