This commit is contained in:
2026-05-30 12:12:14 +02:00
parent 9bace01b93
commit bd81173559
30 changed files with 1020 additions and 51 deletions

View File

@@ -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));
}

View File

@@ -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,
),
),
],

View File

@@ -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'});
},

View File

@@ -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 {

View File

@@ -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,
);

View File

@@ -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');
}
}

View File

@@ -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)
// =========================================================================

View File

@@ -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
View 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',
);
}

View File

@@ -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());
}