From a19fd1104f07763a702aaf73a93558c5d3e301b1 Mon Sep 17 00:00:00 2001 From: Mark M2 Macbook Date: Mon, 20 Apr 2026 23:52:00 +0200 Subject: [PATCH 1/6] aiuto --- lib/core/blocs/session/session_bloc.dart | 101 ----- lib/core/blocs/session/session_cubit.dart | 128 +++++++ lib/core/blocs/session/session_events.dart | 2 +- lib/core/blocs/session/session_state.dart | 80 ++-- lib/core/data/core_repository.dart | 100 +++++ lib/core/routes/app_router.dart | 95 ++--- lib/core/widgets/flux_text_field.dart | 12 +- lib/features/auth/bloc/auth_bloc.dart | 58 --- lib/features/auth/bloc/auth_cubit.dart | 59 +++ lib/features/auth/bloc/auth_events.dart | 21 -- lib/features/auth/bloc/auth_state.dart | 37 +- lib/features/auth/ui/auth_screen.dart | 63 ++-- .../company/data/company_repository.dart | 6 +- .../company/models/company_model.dart | 312 ++++++++++++--- .../company/ui/create_company_screen.dart | 4 +- .../customers/blocs/customer_cubit.dart | 2 +- .../customers/data/customer_repository.dart | 2 +- .../customers/ui/customers_content.dart | 2 +- lib/features/home/ui/dashboard_content.dart | 2 +- lib/features/home/ui/home_screen.dart | 4 +- .../products/blocs/product_cubit.dart | 2 +- .../providers/blocs/provider_cubit.dart | 2 +- .../master_data/staff/blocs/staff_cubit.dart | 2 +- .../staff/models/staff_member_model.dart | 98 +++-- .../master_data/staff/ui/staff_screen.dart | 2 +- .../master_data/store/bloc/store_cubit.dart | 2 +- .../store/ui/create_store_screen.dart | 2 +- .../master_data/store/ui/store_form.dart | 2 +- .../onboarding/blocs/onboarding_cubit.dart | 90 +++++ .../onboarding/blocs/onboarding_state.dart | 37 ++ .../onboarding/ui/onboarding_screen.dart | 355 ++++++++++++++++++ .../onboarding/ui/onboarding_view.dart | 211 +++++++++++ .../services/blocs/services_cubit.dart | 2 +- .../services/data/services_repository.dart | 2 +- .../entertainment_service_card.dart | 2 +- .../services/utils/service_actions.dart | 2 +- lib/main.dart | 71 ++-- 37 files changed, 1546 insertions(+), 428 deletions(-) delete mode 100644 lib/core/blocs/session/session_bloc.dart create mode 100644 lib/core/blocs/session/session_cubit.dart create mode 100644 lib/core/data/core_repository.dart delete mode 100644 lib/features/auth/bloc/auth_bloc.dart create mode 100644 lib/features/auth/bloc/auth_cubit.dart delete mode 100644 lib/features/auth/bloc/auth_events.dart create mode 100644 lib/features/onboarding/blocs/onboarding_cubit.dart create mode 100644 lib/features/onboarding/blocs/onboarding_state.dart create mode 100644 lib/features/onboarding/ui/onboarding_screen.dart create mode 100644 lib/features/onboarding/ui/onboarding_view.dart diff --git a/lib/core/blocs/session/session_bloc.dart b/lib/core/blocs/session/session_bloc.dart deleted file mode 100644 index 1583fce..0000000 --- a/lib/core/blocs/session/session_bloc.dart +++ /dev/null @@ -1,101 +0,0 @@ -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:equatable/equatable.dart'; -import 'package:flux/core/enums/enums.dart'; -import 'package:flux/features/company/models/company_model.dart'; -import 'package:flux/features/master_data/store/data/store_repository.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 'dart:async'; -import 'package:supabase_flutter/supabase_flutter.dart'; - -part 'session_events.dart'; -part 'session_state.dart'; - -class SessionBloc extends Bloc { - final SupabaseClient _supabase = GetIt.I.get(); - final StoreRepository _storeRepository = GetIt.I.get(); - StreamSubscription? _authSubscription; - - SessionBloc() : super(const SessionState(status: SessionStatus.unknown)) { - on((event, emit) { - // 1. Controlla la sessione attuale al boot - final session = _supabase.auth.currentSession; - if (session != null) { - add(UserChanged(session.user.id)); - } else { - add(UserChanged(null)); - } - - // 2. Ascolta i cambiamenti futuri (login, logout, token scaduto) - _authSubscription = _supabase.auth.onAuthStateChange.listen((data) { - final userId = data.session?.user.id; - add(UserChanged(userId)); - }); - }); - - on((event, emit) async { - if (event.userId == null) { - emit(SessionState(status: SessionStatus.unauthenticated)); - return; - } - // 1. Controlla se l'utente ha una Company - final companyJson = await _supabase - .from('company') - .select() - .eq('user_id', event.userId!) - .maybeSingle(); - - if (companyJson == null) { - emit( - SessionState( - status: SessionStatus.authenticatedNoCompany, - userId: event.userId, - ), - ); - return; - } - CompanyModel company = CompanyModel.fromJson(companyJson); - - // 2. Controlla i negozi - final stores = await _storeRepository.fetchAllCompanyStores(company.id); - - if (stores.isEmpty) { - emit( - SessionState( - status: SessionStatus.authenticatedNoStore, - userId: event.userId, - company: company, - ), - ); - return; - } - - // 3. Tutto ok, gestiamo le SharedPreferences per il negozio - final prefs = GetIt.I.get(); - String? lastStoreId = prefs.getString(PrefKeys.lastStore.value); - - // Se non c'è nelle SharedPreferences, prendi il primo della lista - if (lastStoreId == null || !stores.any((s) => s.id == lastStoreId)) { - lastStoreId = stores.first.id; - await prefs.setString('last_store_id', lastStoreId!); - } - final selectedStore = stores.firstWhere((s) => s.id == lastStoreId); - emit( - SessionState( - status: SessionStatus.ready, - userId: event.userId, - company: company, - selectedStore: selectedStore, - availableStores: stores, - ), - ); - }); - } - - @override - Future close() { - _authSubscription?.cancel(); - return super.close(); - } -} diff --git a/lib/core/blocs/session/session_cubit.dart b/lib/core/blocs/session/session_cubit.dart new file mode 100644 index 0000000..f476c2c --- /dev/null +++ b/lib/core/blocs/session/session_cubit.dart @@ -0,0 +1,128 @@ +import 'package:equatable/equatable.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:shared_preferences/shared_preferences.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; +import 'package:collection/collection.dart'; // Per firstWhereOrNull + +// Importa lo state con l'Enum e il CoreRepository... +part 'session_state.dart'; + +class SessionCubit extends Cubit { + final CoreRepository _repository; + final SharedPreferences _prefs; // Iniettato via GetIt + final SupabaseClient _supabase = Supabase.instance.client; + + static const String _lastStoreKey = 'last_selected_store_id'; + + SessionCubit(this._repository, this._prefs) + : super(const SessionState(status: SessionStatus.initial)) { + // Possiamo metterci in ascolto dei cambiamenti di Auth (Login/Logout) + _supabase.auth.onAuthStateChange.listen((data) { + final AuthChangeEvent event = data.event; + if (event == AuthChangeEvent.signedIn) { + initializeSession(); + } else if (event == AuthChangeEvent.signedOut) { + emit(const SessionState(status: SessionStatus.unauthenticated)); + } + }); + } + + Future initializeSession() async { + final user = _supabase.auth.currentUser; + if (user == null) { + return emit(state.copyWith(status: SessionStatus.unauthenticated)); + } + + try { + // 1. Controllo Azienda + final company = await _repository.getCompanyByOwnerId(user.id); + if (company == null) { + return emit( + state.copyWith( + status: SessionStatus.onboardingRequired, + user: user, + onboardingStep: OnboardingStep.company, + ), + ); + } + + // 2. Controllo Negozi + final stores = await _repository.getStoresByCompanyId(company.id); + if (stores.isEmpty) { + return emit( + state.copyWith( + status: SessionStatus.onboardingRequired, + user: user, + company: company, + onboardingStep: OnboardingStep.store, + ), + ); + } + + // 3. Controllo Staff (Paziente Zero) + final staff = await _repository.getStaffMemberByUserId(user.id); + if (staff == null) { + return emit( + state.copyWith( + status: SessionStatus.onboardingRequired, + user: user, + company: company, + onboardingStep: OnboardingStep.staff, + ), + ); + } + + // --- TUTTO COMPLETATO: LOGICA DEL NEGOZIO DI DEFAULT --- + + // Leggiamo l'ultimo negozio dalle SharedPreferences + final lastStoreId = _prefs.getString(_lastStoreKey); + + // Cerchiamo quel negozio nella lista. Se non c'è (magari è stato eliminato), prendiamo il primo. + final activeStore = + stores.firstWhereOrNull((s) => s.id == lastStoreId) ?? stores.first; + + // Se non avevamo il lastStoreId salvato, salviamolo ora + if (lastStoreId != activeStore.id && activeStore.id != null) { + await _prefs.setString(_lastStoreKey, activeStore.id!); + } + + // 4. BENVENUTO A BORDO + emit( + state.copyWith( + status: SessionStatus.authenticated, + user: user, + company: company, + currentStore: activeStore, + currentStaff: staff, + onboardingStep: OnboardingStep.none, // Svuotiamo l'onboarding + ), + ); + } catch (e) { + // Se esplode il database, non lasciamo l'app freezata in 'initial' + emit( + state.copyWith( + status: SessionStatus + .unauthenticated, // O un nuovo stato SessionStatus.error + ), + ); + } + } + + // --- FUNZIONE EXTRA: CAMBIO NEGOZIO DALLA DASHBOARD --- + Future changeStore(StoreModel newStore) async { + if (newStore.id != null) { + await _prefs.setString(_lastStoreKey, newStore.id!); + emit(state.copyWith(currentStore: newStore)); + } + } + + // --- LOGOUT --- + Future signOut() async { + await _supabase.auth.signOut(); + // Non serve emettere stato qui, ci pensa il listener nel costruttore! + } +} diff --git a/lib/core/blocs/session/session_events.dart b/lib/core/blocs/session/session_events.dart index 8f6accb..f2cd441 100644 --- a/lib/core/blocs/session/session_events.dart +++ b/lib/core/blocs/session/session_events.dart @@ -1,4 +1,4 @@ -part of 'session_bloc.dart'; +part of 'session_cubit.dart'; abstract class SessionEvent {} diff --git a/lib/core/blocs/session/session_state.dart b/lib/core/blocs/session/session_state.dart index c85154c..fbcb65e 100644 --- a/lib/core/blocs/session/session_state.dart +++ b/lib/core/blocs/session/session_state.dart @@ -1,51 +1,69 @@ -part of 'session_bloc.dart'; +part of 'session_cubit.dart'; +/// Definisce lo stato macroscopico della sessione enum SessionStatus { - unknown, + initial, unauthenticated, - authenticatedNoCompany, // Loggato ma deve creare l'azienda - authenticatedNoStore, // Ha l'azienda ma deve creare/scegliere il primo negozio - ready, + onboardingRequired, + authenticated, +} + +/// Definisce lo step esatto dell'onboarding (Paranoia Mode) +enum OnboardingStep { + none, // Non serve onboarding + company, // Step 1: Manca l'azienda + store, // Step 2: Manca il negozio + staff, // Step 3: Manca il profilo staff ("Paziente Zero") + completed, // Flusso terminato con successo } class SessionState extends Equatable { final SessionStatus status; - final String? userId; + final User? user; // Utente di Supabase Auth final CompanyModel? company; - final StoreModel? selectedStore; - final List availableStores; // Utile per uno switcher in futuro + final StoreModel? currentStore; + final StaffMemberModel? currentStaff; + final OnboardingStep onboardingStep; const SessionState({ - this.status = SessionStatus.unknown, - this.userId, + this.status = SessionStatus.initial, + this.user, this.company, - this.selectedStore, - this.availableStores = const [], + this.currentStore, + this.currentStaff, + this.onboardingStep = OnboardingStep.none, }); + /// Metodo per creare una copia dello stato modificando solo i campi necessari + SessionState copyWith({ + SessionStatus? status, + User? user, + CompanyModel? company, + StoreModel? currentStore, + StaffMemberModel? currentStaff, + OnboardingStep? onboardingStep, + }) { + return SessionState( + status: status ?? this.status, + user: user ?? this.user, + company: company ?? this.company, + currentStore: currentStore ?? this.currentStore, + currentStaff: currentStaff ?? this.currentStaff, + onboardingStep: onboardingStep ?? this.onboardingStep, + ); + } + @override List get props => [ status, - userId, + user, company, - selectedStore, - availableStores, + currentStore, + currentStaff, + onboardingStep, ]; - // copyWith per aggiornare solo un pezzo (es. quando cambi negozio) - SessionState copyWith({ - SessionStatus? status, - String? userId, - CompanyModel? company, - StoreModel? selectedStore, - List? availableStores, - }) { - return SessionState( - status: status ?? this.status, - userId: userId ?? this.userId, - company: company ?? this.company, - selectedStore: selectedStore ?? this.selectedStore, - availableStores: availableStores ?? this.availableStores, - ); - } + // Helper rapidi per la UI + bool get isAuthenticated => status == SessionStatus.authenticated; + bool get needsOnboarding => status == SessionStatus.onboardingRequired; } diff --git a/lib/core/data/core_repository.dart b/lib/core/data/core_repository.dart new file mode 100644 index 0000000..470f856 --- /dev/null +++ b/lib/core/data/core_repository.dart @@ -0,0 +1,100 @@ +import 'package:flux/features/company/models/company_model.dart'; +import 'package:flux/features/master_data/store/models/store_model.dart'; +import 'package:flux/features/master_data/staff/models/staff_member_model.dart'; +// Importa i tuoi modelli... +import 'package:supabase_flutter/supabase_flutter.dart'; + +class CoreRepository { + final _supabase = Supabase.instance.client; + + // --- QUERY DI SESSIONE (Uso di maybeSingle per evitare crash) --- + + Future getCompanyByOwnerId(String userId) async { + try { + final response = await _supabase + .from('company') + .select() + .eq( + 'owner_id', + userId, + ) // <-- Assicurati di avere questo campo nel DB! + .maybeSingle(); + + if (response == null) return null; + return CompanyModel.fromMap(response); + } catch (e) { + throw Exception('Errore recupero azienda: $e'); + } + } + + Future> getStoresByCompanyId(String companyId) async { + try { + final response = await _supabase + .from('store') + .select() + .eq('company_id', companyId) + .eq('is_active', true) // Buona pratica + .order('name'); // O come si chiama il campo nome + + return (response as List).map((s) => StoreModel.fromMap(s)).toList(); + } catch (e) { + throw Exception('Errore recupero negozi: $e'); + } + } + + Future getStaffMemberByUserId(String userId) async { + try { + final response = await _supabase + .from('staff_member') + .select() + .eq('user_id', userId) + .maybeSingle(); + + if (response == null) return null; + return StaffMemberModel.fromMap(response); + } catch (e) { + throw Exception('Errore recupero profilo staff: $e'); + } + } + + // --- MUTAZIONI PER L'ONBOARDING --- + + Future createCompany(CompanyModel company) async { + try { + final response = await _supabase + .from('company') + .insert(company.toMap()) + .select() + .single(); + return CompanyModel.fromMap(response); + } catch (e) { + throw Exception('Creazione azienda fallita: $e'); + } + } + + Future createStore(StoreModel store) async { + try { + final response = await _supabase + .from('store') + .insert(store.toMap()) + .select() + .single(); + return StoreModel.fromMap(response); + } catch (e) { + throw Exception('Creazione negozio fallita: $e'); + } + } + + Future createStaffMember(StaffMemberModel staff) async { + try { + final response = await _supabase + .from('staff_member') + .insert(staff.toMap()) + .select() + .single(); + return StaffMemberModel.fromMap(response); + } catch (e) { + throw Exception('Creazione profilo staff fallita: $e'); + } + } +} diff --git a/lib/core/routes/app_router.dart b/lib/core/routes/app_router.dart index 3cab5bd..4dc2953 100644 --- a/lib/core/routes/app_router.dart +++ b/lib/core/routes/app_router.dart @@ -1,67 +1,75 @@ +import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:flux/core/blocs/session/session_bloc.dart'; -import 'package:flux/features/auth/ui/auth_screen.dart'; -import 'package:flux/features/company/ui/create_company_screen.dart'; import 'package:flux/features/customers/models/customer_model.dart'; import 'package:flux/features/customers/ui/customer_detail_screen.dart'; -import 'package:flux/features/home/ui/home_screen.dart'; import 'package:flux/features/master_data/products/ui/products_screen.dart'; -import 'package:flux/features/master_data/store/ui/create_store_screen.dart'; import 'package:flux/features/services/models/service_model.dart'; import 'package:flux/features/services/ui/service_form_screen/service_form_screen.dart'; import 'package:go_router/go_router.dart'; -import 'dart:async'; + +// Importa il tuo SessionCubit e lo State +import 'package:flux/core/blocs/session/session_cubit.dart'; class AppRouter { - // Funzione statica per creare il router - static GoRouter createRouter(SessionBloc sessionBloc) { + static GoRouter createRouter(SessionCubit sessionCubit) { return GoRouter( initialLocation: '/', - // Ascolta i cambiamenti del Bloc per scatenare il redirect - refreshListenable: _GoRouterRefreshStream(sessionBloc.stream), + // MAGIA 1: Il router "ascolta" ogni singolo respiro del SessionCubit + refreshListenable: GoRouterRefreshStream(sessionCubit.stream), + + // MAGIA 2: Il Buttafuori Supremo redirect: (context, state) { - final sessionState = sessionBloc.state; + final sessionState = sessionCubit.state; + final isGoingToLogin = state.matchedLocation == '/login'; + final isGoingToOnboarding = state.matchedLocation == '/onboarding'; - // Logica di redirezione basata sugli stati del SessionBloc - final bool isUnknown = sessionState.status == SessionStatus.unknown; - final bool isUnauthenticated = - sessionState.status == SessionStatus.unauthenticated; - final bool isNoCompany = - sessionState.status == SessionStatus.authenticatedNoCompany; - final bool isNoStore = - sessionState.status == SessionStatus.authenticatedNoStore; - final bool isReady = sessionState.status == SessionStatus.ready; - - final String location = state.matchedLocation; - - if (isUnknown) return null; // Aspetta che l'app si svegli - - if (isUnauthenticated && location != '/login') return '/login'; - - if (isNoCompany && location != '/create-company') { - return '/create-company'; + // Caso 1: L'app si sta ancora avviando. + // Restituiamo null per farlo rimanere sulla SplashScreen del main.dart + if (sessionState.status == SessionStatus.initial) { + return null; } - if (isNoStore && location != '/create-store') return '/create-store'; + // Caso 2: Utente NON loggato. + if (sessionState.status == SessionStatus.unauthenticated) { + // Se sta già andando al login, lascialo andare. Altrimenti, forzalo al login. + return isGoingToLogin ? null : '/login'; + } - // Se sono loggato e sto cercando di andare alla login, vai in dashboard - if (isReady && location == '/login') return '/'; + // Caso 3: Utente loggato MA manca un pezzo dell'azienda (Flusso Canalizzatore) + if (sessionState.status == SessionStatus.onboardingRequired) { + // Se sta già andando all'onboarding, ok. Altrimenti forzalo lì. + // Non può "scappare" digitando l'URL della dashboard! + return isGoingToOnboarding ? null : '/onboarding'; + } + + // Caso 4: Utente loggato e configurato (Tutto OK!) + if (sessionState.status == SessionStatus.authenticated) { + // Se per sbaglio cerca di tornare al login o all'onboarding, + // lo rimbalziamo alla home. + if (isGoingToLogin || isGoingToOnboarding) { + return '/'; + } + // Per tutte le altre rotte (dashboard, clienti, anagrafiche), lascialo passare. + return null; + } return null; }, routes: [ - GoRoute(path: '/', builder: (context, state) => const HomeScreen()), GoRoute( path: '/login', - builder: (context, state) => const AuthScreen(), + builder: (context, state) => const LoginScreen(), ), GoRoute( - path: '/create-company', - builder: (context, state) => const CreateCompanyScreen(), + path: '/onboarding', + builder: (context, state) => const OnboardingScreen(), + // Nota: All'interno di questa schermata useremo il PageView pilotato + // dall'OnboardingStep. Al router non interessa quale step è attivo, + // gli basta sapere che deve stare rinchiuso qui dentro! ), GoRoute( - path: '/create-store', - builder: (context, state) => const CreateStoreScreen(), + path: '/', + builder: (context, state) => const DashboardScreen(), // La tua home ), GoRoute( path: '/customer/:id', @@ -96,11 +104,14 @@ class AppRouter { } } -// Classe di supporto per convertire lo Stream del Bloc in un Listenable per GoRouter -class _GoRouterRefreshStream extends ChangeNotifier { - _GoRouterRefreshStream(Stream stream) { +/// Utility fondamentale per GoRouter: trasforma lo Stream del Cubit +/// in un Listenable che GoRouter può ascoltare per forzare i redirect. +class GoRouterRefreshStream extends ChangeNotifier { + GoRouterRefreshStream(Stream stream) { notifyListeners(); - _subscription = stream.asBroadcastStream().listen((_) => notifyListeners()); + _subscription = stream.asBroadcastStream().listen( + (dynamic _) => notifyListeners(), + ); } late final StreamSubscription _subscription; diff --git a/lib/core/widgets/flux_text_field.dart b/lib/core/widgets/flux_text_field.dart index a60afe5..da7055b 100644 --- a/lib/core/widgets/flux_text_field.dart +++ b/lib/core/widgets/flux_text_field.dart @@ -4,7 +4,7 @@ import 'package:flux/core/theme/theme.dart'; class FluxTextField extends StatefulWidget { final String label; - final IconData icon; + final IconData? icon; final bool isPassword; final bool autoFocus; final TextEditingController? controller; @@ -14,11 +14,12 @@ class FluxTextField extends StatefulWidget { final Function(String)? onSubmitted; final Function(String)? onChanged; final int? maxLength; + final String? Function(String?)? validator; const FluxTextField({ super.key, // Usiamo super.key per Flutter moderno required this.label, - required this.icon, + this.icon, this.isPassword = false, this.autoFocus = false, this.controller, @@ -28,6 +29,7 @@ class FluxTextField extends StatefulWidget { this.onSubmitted, this.onChanged, this.maxLength, + this.validator, }); @override @@ -45,8 +47,9 @@ class _FluxTextFieldState extends State { @override Widget build(BuildContext context) { - return TextField( + return TextFormField( controller: widget.controller, + validator: widget.validator, obscureText: _obscureText, enableSuggestions: !widget.isPassword, autocorrect: !widget.isPassword, @@ -79,6 +82,7 @@ class _FluxTextFieldState extends State { horizontal: 16, vertical: 16, ), + suffixIcon: widget.isPassword ? IconButton( icon: Icon( @@ -95,7 +99,7 @@ class _FluxTextFieldState extends State { ) : null, // Se non è una password, niente icona ), - onSubmitted: widget.onSubmitted, + onFieldSubmitted: widget.onSubmitted, onChanged: widget.onChanged, maxLength: widget.maxLength, ); diff --git a/lib/features/auth/bloc/auth_bloc.dart b/lib/features/auth/bloc/auth_bloc.dart deleted file mode 100644 index 202136b..0000000 --- a/lib/features/auth/bloc/auth_bloc.dart +++ /dev/null @@ -1,58 +0,0 @@ -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:equatable/equatable.dart'; -import 'package:get_it/get_it.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; - -part 'auth_events.dart'; -part 'auth_state.dart'; - -class AuthBloc extends Bloc { - final _supabase = GetIt.instance(); - - AuthBloc() - : super(const AuthState(status: AuthStatus.initial, isLoginMode: true)) { - on( - (event, emit) => emit(state.copyWith(isLoginMode: !state.isLoginMode)), - ); - on((event, emit) async { - emit(state.copyWith(status: AuthStatus.loading)); - try { - if (state.isLoginMode) { - // --- LOGICA LOGIN --- - await _supabase.auth.signInWithPassword( - email: event.email, - password: event.password, - ); - // Non serve emettere success qui, ci pensa il SessionBloc! - } else { - // --- LOGICA SIGNUP --- - await _supabase.auth.signUp( - email: event.email, - password: event.password, - ); - - // Nota: Se Supabase richiede conferma email, l'utente non sarà - // loggato subito. Gestiamolo con un messaggio. - emit( - state.copyWith( - status: AuthStatus.success, - error: "Controlla la tua email per confermare l'account!", - ), - ); - } - } on AuthException catch (e) { - emit(state.copyWith(status: AuthStatus.failure, error: e.message)); - } catch (e) { - emit( - state.copyWith( - status: AuthStatus.failure, - error: "Errore imprevisto: $e", - ), - ); - } - }); - on((event, emit) async { - await _supabase.auth.signOut(); - }); - } -} diff --git a/lib/features/auth/bloc/auth_cubit.dart b/lib/features/auth/bloc/auth_cubit.dart new file mode 100644 index 0000000..734cb22 --- /dev/null +++ b/lib/features/auth/bloc/auth_cubit.dart @@ -0,0 +1,59 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:get_it/get_it.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; +part 'auth_state.dart'; + +class AuthCubit extends Cubit { + final _supabase = GetIt.instance(); + + AuthCubit() : super(const AuthState()); + + void toggleMode() { + emit(state.copyWith(isLoginMode: !state.isLoginMode)); + } + + Future submitAuth(String email, String password) async { + // Partiamo puliti: via vecchi messaggi ed errori + emit(state.copyWith(status: AuthStatus.loading)); + + try { + if (state.isLoginMode) { + // --- LOGICA LOGIN --- + await _supabase.auth.signInWithPassword( + email: email, + password: password, + ); + // NESSUN EMIT DI SUCCESS! + // Supabase lancerà l'evento 'signedIn', il SessionCubit lo catturerà + // e il GoRouter ci cambierà pagina. Noi stiamo a guardare il caricamento. + } else { + // --- LOGICA SIGNUP --- + final AuthResponse res = await _supabase.auth.signUp( + email: email, + password: password, + ); + + // Se la sessione è null, significa che Supabase ha inviato l'email di conferma + if (res.session == null) { + emit( + state.copyWith( + status: AuthStatus.initial, + infoMessage: "Controlla la tua email per confermare l'account!", + ), + ); + } + // Se non è null, ha fatto il login automatico. Stessa cosa di sopra, ci pensa il SessionCubit. + } + } on AuthException catch (e) { + emit(state.copyWith(status: AuthStatus.failure, errorMessage: e.message)); + } catch (e) { + emit( + state.copyWith( + status: AuthStatus.failure, + errorMessage: "Errore imprevisto: $e", + ), + ); + } + } +} diff --git a/lib/features/auth/bloc/auth_events.dart b/lib/features/auth/bloc/auth_events.dart deleted file mode 100644 index da08631..0000000 --- a/lib/features/auth/bloc/auth_events.dart +++ /dev/null @@ -1,21 +0,0 @@ -part of 'auth_bloc.dart'; - -abstract class AuthEvent extends Equatable { - const AuthEvent(); - - @override - List get props => []; -} - -class ToggleAuthMode extends AuthEvent {} // Passa da Login a Registrazione - -class LoginRequested extends AuthEvent { - final String email; - final String password; - const LoginRequested({required this.email, required this.password}); - - @override - List get props => [email, password]; -} - -class LogoutRequested extends AuthEvent {} // Logout diff --git a/lib/features/auth/bloc/auth_state.dart b/lib/features/auth/bloc/auth_state.dart index 2fb1a87..e491094 100644 --- a/lib/features/auth/bloc/auth_state.dart +++ b/lib/features/auth/bloc/auth_state.dart @@ -1,26 +1,35 @@ -part of 'auth_bloc.dart'; +part of 'auth_cubit.dart'; -enum AuthStatus { initial, loading, success, failure } +enum AuthStatus { initial, loading, failure } class AuthState extends Equatable { + final AuthStatus status; + final bool isLoginMode; + final String? errorMessage; + final String? infoMessage; + const AuthState({ - required this.status, - this.error, - required this.isLoginMode, + this.status = AuthStatus.initial, + this.isLoginMode = true, + this.errorMessage, + this.infoMessage, }); - final AuthStatus status; - final String? error; - final bool isLoginMode; - - @override - List get props => [status, error, isLoginMode]; - - AuthState copyWith({AuthStatus? status, String? error, bool? isLoginMode}) { + AuthState copyWith({ + AuthStatus? status, + bool? isLoginMode, + String? errorMessage, + String? infoMessage, + }) { return AuthState( status: status ?? this.status, - error: error, isLoginMode: isLoginMode ?? this.isLoginMode, + // Se non passo esplicitamente un errore, lo resetto per evitare che rimanga bloccato a schermo + errorMessage: errorMessage, + infoMessage: infoMessage, ); } + + @override + List get props => [status, isLoginMode, errorMessage, infoMessage]; } diff --git a/lib/features/auth/ui/auth_screen.dart b/lib/features/auth/ui/auth_screen.dart index 96a37e3..0f3486e 100644 --- a/lib/features/auth/ui/auth_screen.dart +++ b/lib/features/auth/ui/auth_screen.dart @@ -3,7 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/theme/theme.dart'; import 'package:flux/core/widgets/flux_logo.dart'; import 'package:flux/core/widgets/flux_text_field.dart'; -import 'package:flux/features/auth/bloc/auth_bloc.dart'; +import 'package:flux/features/auth/bloc/auth_cubit.dart'; class AuthScreen extends StatefulWidget { const AuthScreen({super.key}); @@ -15,7 +15,6 @@ class AuthScreen extends StatefulWidget { class _AuthScreenState extends State { final _emailController = TextEditingController(); final _passwordController = TextEditingController(); - final _isPassword = true; @override void dispose() { @@ -24,19 +23,43 @@ class _AuthScreenState extends State { super.dispose(); } + void _submit() { + // Chiudiamo la tastiera per fare pulizia a schermo + FocusScope.of(context).unfocus(); + + context.read().submitAuth( + _emailController.text.trim(), + _passwordController.text.trim(), + ); + } + @override Widget build(BuildContext context) { return Scaffold( - body: BlocConsumer( + body: BlocConsumer( + // Ottimizzazione: Ridisegniamo la UI solo quando cambia lo status o la modalità + listenWhen: (previous, current) => + previous.errorMessage != current.errorMessage || + previous.infoMessage != current.infoMessage, listener: (context, state) { - if (state.status == AuthStatus.failure) { + // Mostriamo l'errore se c'è + if (state.errorMessage != null) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(state.error ?? 'Errore di autenticazione'), + content: Text(state.errorMessage!), backgroundColor: Colors.redAccent, ), ); } + // Mostriamo il messaggio info (es. Conferma Email) + if (state.infoMessage != null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.infoMessage!), + backgroundColor: Colors.blueAccent, // O context.accent + ), + ); + } }, builder: (context, state) { final isLoading = state.status == AuthStatus.loading; @@ -49,7 +72,7 @@ class _AuthScreenState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ // --- LOGO FLUX --- - FluxLogoAuto(height: 80), + const FluxLogoAuto(height: 80), const SizedBox(height: 60), // --- TITOLO DINAMICO --- @@ -77,15 +100,15 @@ class _AuthScreenState extends State { label: 'Email Aziendale', icon: Icons.email_outlined, controller: _emailController, - keyboardType: TextInputType.emailAddress, + // TODO: Aggiungi nel tuo FluxTextField la gestione del keyboardType se non c'è già! ), const SizedBox(height: 20), FluxTextField( label: 'Password', icon: Icons.lock_outline, - isPassword: true, + isPassword: true, // Magia del FluxTextField! controller: _passwordController, - onSubmitted: (_) => _submit(), + // onSubmitted: (_) => _submit(), // Se lo supporti nel tuo widget custom ), const SizedBox(height: 40), @@ -95,7 +118,7 @@ class _AuthScreenState extends State { width: double.infinity, height: 56, child: ElevatedButton( - onPressed: isLoading ? null : () => _submit(), + onPressed: isLoading ? null : _submit, child: isLoading ? const SizedBox( height: 24, @@ -105,7 +128,12 @@ class _AuthScreenState extends State { color: Colors.white, ), ) - : Text(state.isLoginMode ? 'ACCEDI' : 'REGISTRATI'), + : Text( + state.isLoginMode ? 'ACCEDI' : 'REGISTRATI', + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), ), ), @@ -114,9 +142,7 @@ class _AuthScreenState extends State { TextButton( onPressed: isLoading ? null - : () { - context.read().add(ToggleAuthMode()); - }, + : () => context.read().toggleMode(), child: RichText( text: TextSpan( text: state.isLoginMode @@ -144,13 +170,4 @@ class _AuthScreenState extends State { ), ); } - - void _submit() { - context.read().add( - LoginRequested( - email: _emailController.text.trim(), - password: _passwordController.text.trim(), - ), - ); - } } diff --git a/lib/features/company/data/company_repository.dart b/lib/features/company/data/company_repository.dart index 8416a87..2c70f4f 100644 --- a/lib/features/company/data/company_repository.dart +++ b/lib/features/company/data/company_repository.dart @@ -9,11 +9,11 @@ class CompanyRepository { // .select().single() trasforma la risposta nell'oggetto appena inserito final response = await _supabase .from('company') - .insert(company.toJson()) + .insert(company.toMap()) .select() .single(); - return CompanyModel.fromJson(response); + return CompanyModel.fromMap(response); } on PostgrestException catch (e) { throw e.message; } catch (e) { @@ -30,7 +30,7 @@ class CompanyRepository { .eq('user_id', userId as Object) .maybeSingle(); - return response != null ? CompanyModel.fromJson(response) : null; + return response != null ? CompanyModel.fromMap(response) : null; } catch (e) { return null; } diff --git a/lib/features/company/models/company_model.dart b/lib/features/company/models/company_model.dart index 332bae3..65406af 100644 --- a/lib/features/company/models/company_model.dart +++ b/lib/features/company/models/company_model.dart @@ -1,9 +1,50 @@ import 'package:equatable/equatable.dart'; +// =================================================================== +// ENUMS (Paranoia Mode per la lettura dal DB) +// =================================================================== + +enum SubscriptionTier { + free, + pro, + premium, + platinum; + + static SubscriptionTier fromString(String? value) { + return SubscriptionTier.values.firstWhere( + (e) => e.name == value, + orElse: () => SubscriptionTier.free, // Fallback di sicurezza + ); + } +} + +enum SubscriptionStatus { + trialing, + active, + pastDue, + canceled; + + static SubscriptionStatus fromString(String? value) { + if (value == null) return SubscriptionStatus.canceled; + // Normalizziamo 'past_due' dal DB in 'pastdue' per il match con l'Enum + final normalizedValue = value.replaceAll('_', '').toLowerCase(); + return SubscriptionStatus.values.firstWhere( + (e) => e.name.toLowerCase() == normalizedValue, + orElse: () => SubscriptionStatus.canceled, + ); + } +} + +// =================================================================== +// IL MODELLO ESATTO +// =================================================================== + class CompanyModel extends Equatable { - final String id; + final String? id; final DateTime? createdAt; - final String userId; + final String userId; // Nel DB è user_id (chiave esterna su auth.users) + + // Dati Anagrafici e Fatturazione final String ragioneSociale; final String indirizzo; final String cap; @@ -12,12 +53,21 @@ class CompanyModel extends Equatable { final String partitaIva; final String codiceFiscale; final String codiceUnivoco; - final bool isPaid; - final DateTime? paymentExpiration; final String companyLogo; + // Stato Pagamenti (Ibride: manuale + Stripe) + final bool isPaid; + final DateTime? paymentExpiration; + + // Campi SaaS Stripe/Automazioni + final SubscriptionTier subscriptionTier; + final SubscriptionStatus subscriptionStatus; + final DateTime? trialEndsAt; + final String? stripeCustomerId; + final String? stripeSubscriptionId; + const CompanyModel({ - this.id = '', + this.id, this.createdAt, required this.userId, required this.ragioneSociale, @@ -28,48 +78,16 @@ class CompanyModel extends Equatable { required this.partitaIva, required this.codiceFiscale, required this.codiceUnivoco, + this.companyLogo = '', this.isPaid = false, this.paymentExpiration, - this.companyLogo = '', + this.subscriptionTier = SubscriptionTier.free, + this.subscriptionStatus = SubscriptionStatus.trialing, + this.trialEndsAt, + this.stripeCustomerId, + this.stripeSubscriptionId, }); - factory CompanyModel.fromJson(Map json) { - return CompanyModel( - id: json['id'] as String, - createdAt: DateTime.parse(json['created_at']), - userId: json['user_id'] as String, - ragioneSociale: json['ragione_sociale'], - indirizzo: json['indirizzo'], - cap: json['cap'], - citta: json['citta'], - provincia: json['provincia'], - partitaIva: json['partita_iva'], - codiceFiscale: json['codice_fiscale'], - codiceUnivoco: json['codice_univoco'], - isPaid: json['is_paid'] ?? false, - paymentExpiration: json['payment_expiration'] != null - ? DateTime.parse(json['payment_expiration']) - : null, - companyLogo: json['company_logo'] ?? '', - ); - } - - Map toJson() { - return { - 'ragione_sociale': ragioneSociale, - 'indirizzo': indirizzo, - 'cap': cap, - 'citta': citta, - 'provincia': provincia, - 'partita_iva': partitaIva, - 'codice_fiscale': codiceFiscale, - 'codice_univoco': codiceUnivoco, - 'is_paid': isPaid, - 'payment_expiration': paymentExpiration?.toIso8601String(), - 'company_logo': companyLogo, - }; - } - CompanyModel copyWith({ String? id, DateTime? createdAt, @@ -82,26 +100,198 @@ class CompanyModel extends Equatable { String? partitaIva, String? codiceFiscale, String? codiceUnivoco, + String? companyLogo, bool? isPaid, DateTime? paymentExpiration, - String? companyLogo, - }) => CompanyModel( - id: id ?? this.id, - createdAt: createdAt ?? this.createdAt, - userId: userId ?? this.userId, - ragioneSociale: ragioneSociale ?? this.ragioneSociale, - indirizzo: indirizzo ?? this.indirizzo, - cap: cap ?? this.cap, - citta: citta ?? this.citta, - provincia: provincia ?? this.provincia, - partitaIva: partitaIva ?? this.partitaIva, - codiceFiscale: codiceFiscale ?? this.codiceFiscale, - codiceUnivoco: codiceUnivoco ?? this.codiceUnivoco, - isPaid: isPaid ?? this.isPaid, - paymentExpiration: paymentExpiration ?? this.paymentExpiration, - companyLogo: companyLogo ?? this.companyLogo, - ); + SubscriptionTier? subscriptionTier, + SubscriptionStatus? subscriptionStatus, + DateTime? trialEndsAt, + String? stripeCustomerId, + String? stripeSubscriptionId, + }) { + return CompanyModel( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + userId: userId ?? this.userId, + ragioneSociale: ragioneSociale ?? this.ragioneSociale, + indirizzo: indirizzo ?? this.indirizzo, + cap: cap ?? this.cap, + citta: citta ?? this.citta, + provincia: provincia ?? this.provincia, + partitaIva: partitaIva ?? this.partitaIva, + codiceFiscale: codiceFiscale ?? this.codiceFiscale, + codiceUnivoco: codiceUnivoco ?? this.codiceUnivoco, + companyLogo: companyLogo ?? this.companyLogo, + isPaid: isPaid ?? this.isPaid, + paymentExpiration: paymentExpiration ?? this.paymentExpiration, + subscriptionTier: subscriptionTier ?? this.subscriptionTier, + subscriptionStatus: subscriptionStatus ?? this.subscriptionStatus, + trialEndsAt: trialEndsAt ?? this.trialEndsAt, + stripeCustomerId: stripeCustomerId ?? this.stripeCustomerId, + stripeSubscriptionId: stripeSubscriptionId ?? this.stripeSubscriptionId, + ); + } + + factory CompanyModel.fromMap(Map map) { + return CompanyModel( + id: map['id'] as String?, + createdAt: map['created_at'] != null + ? DateTime.tryParse(map['created_at']) + : null, + userId: map['user_id'] ?? '', + ragioneSociale: map['ragione_sociale'] ?? '', + indirizzo: map['indirizzo'] ?? '', + cap: map['cap'] ?? '', + citta: map['citta'] ?? '', + provincia: map['provincia'] ?? '', + partitaIva: map['partita_iva'] ?? '', + codiceFiscale: map['codice_fiscale'] ?? '', + codiceUnivoco: map['codice_univoco'] ?? '', + companyLogo: map['company_logo'] ?? '', + isPaid: map['is_paid'] ?? false, + paymentExpiration: map['payment_expiration'] != null + ? DateTime.tryParse(map['payment_expiration']) + : null, + subscriptionTier: SubscriptionTier.fromString(map['subscription_tier']), + subscriptionStatus: SubscriptionStatus.fromString( + map['subscription_status'], + ), + trialEndsAt: map['trial_ends_at'] != null + ? DateTime.tryParse(map['trial_ends_at']) + : null, + stripeCustomerId: map['stripe_customer_id'], + stripeSubscriptionId: map['stripe_subscription_id'], + ); + } + + Map toMap() { + return { + if (id != null) 'id': id, + // created_at è gestito dal DB di default, di solito non si passa nell'insert + 'user_id': userId, + 'ragione_sociale': ragioneSociale, + 'indirizzo': indirizzo, + 'cap': cap, + 'citta': citta, + 'provincia': provincia, + 'partita_iva': partitaIva, + 'codice_fiscale': codiceFiscale, + 'codice_univoco': codiceUnivoco, + 'company_logo': companyLogo, + 'is_paid': isPaid, + if (paymentExpiration != null) + 'payment_expiration': paymentExpiration!.toIso8601String(), + 'subscription_tier': subscriptionTier.name, + 'subscription_status': subscriptionStatus == SubscriptionStatus.pastDue + ? 'past_due' + : subscriptionStatus.name, + if (trialEndsAt != null) 'trial_ends_at': trialEndsAt!.toIso8601String(), + if (stripeCustomerId != null) 'stripe_customer_id': stripeCustomerId, + if (stripeSubscriptionId != null) + 'stripe_subscription_id': stripeSubscriptionId, + }; + } @override - List get props => [id, userId, ragioneSociale, partitaIva, isPaid]; + List get props => [ + id, + createdAt, + userId, + ragioneSociale, + indirizzo, + cap, + citta, + provincia, + partitaIva, + codiceFiscale, + codiceUnivoco, + companyLogo, + isPaid, + paymentExpiration, + subscriptionTier, + subscriptionStatus, + trialEndsAt, + stripeCustomerId, + stripeSubscriptionId, + ]; +} + +// =================================================================== +// BUSINESS LOGIC: I LIMITI DEI TIER +// =================================================================== + +extension CompanyLimits on CompanyModel { + int get maxStores { + switch (subscriptionTier) { + case SubscriptionTier.free: + return 1; + case SubscriptionTier.pro: + return 1; + case SubscriptionTier.premium: + return 10; + case SubscriptionTier.platinum: + return 30; + } + } + + int get maxStaffMembers { + switch (subscriptionTier) { + case SubscriptionTier.free: + return 2; + case SubscriptionTier.pro: + return 10; + case SubscriptionTier.premium: + return 50; + case SubscriptionTier.platinum: + return 150; + } + } + + int get maxServicesPerMonth { + switch (subscriptionTier) { + case SubscriptionTier.free: + return 50; + case SubscriptionTier.pro: + return 1000; + case SubscriptionTier.premium: + return 10000; + case SubscriptionTier.platinum: + return 30000; + } + } + + int get maxStorageGb { + switch (subscriptionTier) { + case SubscriptionTier.free: + return 0; // 500MB = 0.5 GB, magari qui usi i MB interi + case SubscriptionTier.pro: + return 3; + case SubscriptionTier.premium: + return 30; + case SubscriptionTier.platinum: + return 100; + } + } + + /// Verifica generale: L'utente ha i permessi per usare l'app? + bool get hasActiveAccess { + // 1. Priorità all'override manuale (is_paid e payment_expiration) + if (isPaid) { + if (paymentExpiration == null) + return true; // Pagato "a vita" o senza scadenza + if (DateTime.now().isBefore(paymentExpiration!)) return true; + } + + // 2. Controllo SaaS (Stripe/Subscription) + if (subscriptionStatus == SubscriptionStatus.active) return true; + + // 3. Controllo Trial + if (subscriptionStatus == SubscriptionStatus.trialing && + trialEndsAt != null) { + return DateTime.now().isBefore(trialEndsAt!); + } + + // Scaduto, past_due, o cancellato + return false; + } } diff --git a/lib/features/company/ui/create_company_screen.dart b/lib/features/company/ui/create_company_screen.dart index 255e2a0..fa2ecf6 100644 --- a/lib/features/company/ui/create_company_screen.dart +++ b/lib/features/company/ui/create_company_screen.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/features/auth/bloc/auth_bloc.dart'; +import 'package:flux/features/auth/bloc/auth_cubit.dart'; import 'package:flux/features/company/bloc/company_bloc.dart'; -import 'package:flux/core/blocs/session/session_bloc.dart'; +import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/theme/theme.dart'; import 'package:flux/core/widgets/flux_text_field.dart'; import 'package:flux/features/company/models/company_model.dart'; diff --git a/lib/features/customers/blocs/customer_cubit.dart b/lib/features/customers/blocs/customer_cubit.dart index 7f39674..248913c 100644 --- a/lib/features/customers/blocs/customer_cubit.dart +++ b/lib/features/customers/blocs/customer_cubit.dart @@ -1,7 +1,7 @@ import 'dart:async'; // Serve per il Timer del debounce import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:equatable/equatable.dart'; -import 'package:flux/core/blocs/session/session_bloc.dart'; +import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/features/customers/data/customer_repository.dart'; import 'package:flux/features/customers/models/customer_model.dart'; import 'package:get_it/get_it.dart'; diff --git a/lib/features/customers/data/customer_repository.dart b/lib/features/customers/data/customer_repository.dart index ddbef62..463226e 100644 --- a/lib/features/customers/data/customer_repository.dart +++ b/lib/features/customers/data/customer_repository.dart @@ -1,5 +1,5 @@ import 'package:file_picker/file_picker.dart'; -import 'package:flux/core/blocs/session/session_bloc.dart'; +import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/utils/string_extensions.dart'; import 'package:flux/features/customers/models/customer_file_model.dart'; import 'package:get_it/get_it.dart'; diff --git a/lib/features/customers/ui/customers_content.dart b/lib/features/customers/ui/customers_content.dart index e7c22dd..6f46905 100644 --- a/lib/features/customers/ui/customers_content.dart +++ b/lib/features/customers/ui/customers_content.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/core/blocs/session/session_bloc.dart'; +import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/theme/theme.dart'; import 'package:flux/features/customers/blocs/customer_cubit.dart'; import 'package:flux/features/customers/models/customer_model.dart'; diff --git a/lib/features/home/ui/dashboard_content.dart b/lib/features/home/ui/dashboard_content.dart index 997f5ff..3c82a27 100644 --- a/lib/features/home/ui/dashboard_content.dart +++ b/lib/features/home/ui/dashboard_content.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/core/blocs/session/session_bloc.dart'; +import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/theme/theme.dart'; import 'package:flux/features/home/ui/dashboard_adaptive_grid.dart'; diff --git a/lib/features/home/ui/home_screen.dart b/lib/features/home/ui/home_screen.dart index fbde4fd..d0f3c39 100644 --- a/lib/features/home/ui/home_screen.dart +++ b/lib/features/home/ui/home_screen.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/core/blocs/session/session_bloc.dart'; +import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/theme/theme.dart'; -import 'package:flux/features/auth/bloc/auth_bloc.dart'; +import 'package:flux/features/auth/bloc/auth_cubit.dart'; import 'package:flux/features/master_data/master_data_hub_content.dart'; import 'package:flux/features/services/blocs/services_cubit.dart'; import 'package:flux/features/services/ui/services_screen.dart'; diff --git a/lib/features/master_data/products/blocs/product_cubit.dart b/lib/features/master_data/products/blocs/product_cubit.dart index b290a2a..973dd35 100644 --- a/lib/features/master_data/products/blocs/product_cubit.dart +++ b/lib/features/master_data/products/blocs/product_cubit.dart @@ -1,7 +1,7 @@ import 'package:collection/collection.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/core/blocs/session/session_bloc.dart'; +import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/features/master_data/products/data/product_repository.dart'; import 'package:flux/features/master_data/products/models/brand_model.dart'; import 'package:flux/features/master_data/products/models/model_model.dart'; diff --git a/lib/features/master_data/providers/blocs/provider_cubit.dart b/lib/features/master_data/providers/blocs/provider_cubit.dart index 0a7ee5b..857c932 100644 --- a/lib/features/master_data/providers/blocs/provider_cubit.dart +++ b/lib/features/master_data/providers/blocs/provider_cubit.dart @@ -1,6 +1,6 @@ import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/core/blocs/session/session_bloc.dart'; +import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/features/master_data/providers/data/provider_repository.dart'; import 'package:flux/features/master_data/store/models/store_model.dart'; import 'package:get_it/get_it.dart'; diff --git a/lib/features/master_data/staff/blocs/staff_cubit.dart b/lib/features/master_data/staff/blocs/staff_cubit.dart index 64a14b9..4c3fb60 100644 --- a/lib/features/master_data/staff/blocs/staff_cubit.dart +++ b/lib/features/master_data/staff/blocs/staff_cubit.dart @@ -1,6 +1,6 @@ import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/core/blocs/session/session_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/master_data/store/models/store_model.dart'; diff --git a/lib/features/master_data/staff/models/staff_member_model.dart b/lib/features/master_data/staff/models/staff_member_model.dart index 46ea409..3f8a1c5 100644 --- a/lib/features/master_data/staff/models/staff_member_model.dart +++ b/lib/features/master_data/staff/models/staff_member_model.dart @@ -1,47 +1,101 @@ import 'package:equatable/equatable.dart'; -import 'package:flux/core/utils/string_extensions.dart'; // Assicurati che il percorso sia corretto + +// L'Enum magico e blindato per il sistema +enum SystemRole { + admin, + manager, + user; + + // Helper per convertire dal DB a Dart in sicurezza + static SystemRole fromString(String? value) { + return SystemRole.values.firstWhere( + (e) => e.name == value, + orElse: () => SystemRole.user, // Fallback paranoico al livello più basso + ); + } +} class StaffMemberModel extends Equatable { final String? id; - final String name; - final String email; - final String phone; - final bool isActive; final String companyId; + final String storeId; + final String userId; + final String name; + final String surname; + final String? + jobTitle; // Testo libero! Il cliente ci scrive quello che vuole. + final SystemRole systemRole; // ENUM! Il sistema non si frega. const StaffMemberModel({ this.id, - required this.name, - this.email = '', - this.phone = '', - this.isActive = true, required this.companyId, + required this.storeId, + required this.userId, + required this.name, + required this.surname, + this.jobTitle, + this.systemRole = SystemRole.user, // Sicurezza di default }); + StaffMemberModel copyWith({ + String? id, + String? companyId, + String? storeId, + String? userId, + String? name, + String? surname, + String? jobTitle, + SystemRole? systemRole, + }) { + return StaffMemberModel( + id: id ?? this.id, + companyId: companyId ?? this.companyId, + storeId: storeId ?? this.storeId, + userId: userId ?? this.userId, + name: name ?? this.name, + surname: surname ?? this.surname, + jobTitle: jobTitle ?? this.jobTitle, + systemRole: systemRole ?? this.systemRole, + ); + } + factory StaffMemberModel.fromMap(Map map) { return StaffMemberModel( - id: map['id'], - // Applichiamo il tuo myFormat per visualizzare i nomi correttamente - name: (map['name'] as String).myFormat(), - // L'email la teniamo lowercase per standard tecnico - email: (map['email'] as String? ?? '').toLowerCase().trim(), - phone: (map['phone'] as String? ?? '').trim(), - isActive: map['is_active'] ?? true, - companyId: map['company_id'], + id: map['id'] as String?, + companyId: map['company_id'] ?? '', + storeId: map['store_id'] ?? '', + userId: map['user_id'] ?? '', + name: map['name'] ?? '', + surname: map['surname'] ?? '', + jobTitle: map['job_title'] as String?, // Semplice stringa + systemRole: SystemRole.fromString( + map['system_role'], + ), // Lettura tipizzata ); } Map toMap() { return { if (id != null) 'id': id, - 'name': name.toLowerCase().trim(), // Salviamo pulito per le query - 'email': email.toLowerCase().trim(), - 'phone': phone.trim(), - 'is_active': isActive, 'company_id': companyId, + 'store_id': storeId, + 'user_id': userId, + 'name': name, + 'surname': surname, + if (jobTitle != null) 'job_title': jobTitle, + 'system_role': systemRole.name, // Trasforma SystemRole.admin in 'admin' }; } @override - List get props => [id, name, email, phone, isActive, companyId]; + List get props => [ + id, + companyId, + storeId, + userId, + name, + surname, + jobTitle, + systemRole, + ]; } diff --git a/lib/features/master_data/staff/ui/staff_screen.dart b/lib/features/master_data/staff/ui/staff_screen.dart index a8c0a93..b1dd3bb 100644 --- a/lib/features/master_data/staff/ui/staff_screen.dart +++ b/lib/features/master_data/staff/ui/staff_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/core/blocs/session/session_bloc.dart'; +import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/theme/theme.dart'; import 'package:flux/core/widgets/flux_text_field.dart'; import 'package:flux/features/master_data/staff/blocs/staff_cubit.dart'; // Tuo percorso diff --git a/lib/features/master_data/store/bloc/store_cubit.dart b/lib/features/master_data/store/bloc/store_cubit.dart index 8d34f5d..9d83387 100644 --- a/lib/features/master_data/store/bloc/store_cubit.dart +++ b/lib/features/master_data/store/bloc/store_cubit.dart @@ -1,6 +1,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:equatable/equatable.dart'; -import 'package:flux/core/blocs/session/session_bloc.dart'; +import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/features/master_data/providers/models/provider_model.dart'; import 'package:flux/features/master_data/staff/data/staff_repository.dart'; import 'package:flux/features/master_data/staff/models/staff_member_model.dart'; diff --git a/lib/features/master_data/store/ui/create_store_screen.dart b/lib/features/master_data/store/ui/create_store_screen.dart index 1b3c9e8..77fd41c 100644 --- a/lib/features/master_data/store/ui/create_store_screen.dart +++ b/lib/features/master_data/store/ui/create_store_screen.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/features/master_data/store/bloc/store_cubit.dart'; import 'package:flux/features/master_data/store/models/store_model.dart'; -import 'package:flux/core/blocs/session/session_bloc.dart'; +import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/theme/theme.dart'; import 'package:flux/core/widgets/flux_text_field.dart'; diff --git a/lib/features/master_data/store/ui/store_form.dart b/lib/features/master_data/store/ui/store_form.dart index ee0b88b..d9be617 100644 --- a/lib/features/master_data/store/ui/store_form.dart +++ b/lib/features/master_data/store/ui/store_form.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/core/blocs/session/session_bloc.dart'; +import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/widgets/flux_text_field.dart'; import 'package:flux/features/master_data/store/bloc/store_cubit.dart'; import 'package:flux/features/master_data/store/models/store_model.dart'; diff --git a/lib/features/onboarding/blocs/onboarding_cubit.dart b/lib/features/onboarding/blocs/onboarding_cubit.dart new file mode 100644 index 0000000..82dbea0 --- /dev/null +++ b/lib/features/onboarding/blocs/onboarding_cubit.dart @@ -0,0 +1,90 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flux/core/blocs/session/session_cubit.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:flux/features/onboarding/blocs/onboarding_state.dart'; + +class OnboardingCubit extends Cubit { + final CoreRepository _repository; + final SessionCubit _sessionCubit; + + OnboardingCubit(this._sessionCubit, this._repository) + : super(OnboardingState(step: _sessionCubit.state.onboardingStep)); + + // --- STEP 1: REGISTRAZIONE AZIENDA --- + Future saveCompany(CompanyModel company) async { + emit(state.copyWith(isLoading: true)); + try { + // Il repository restituisce il modello creato con l'ID di Supabase + final savedCompany = await _repository.createCompany(company); + + emit( + state.copyWith( + isLoading: false, + step: OnboardingStep.store, + companyId: savedCompany.id, + ), + ); + } catch (e) { + emit( + state.copyWith( + isLoading: false, + error: "Errore salvataggio azienda: $e", + ), + ); + } + } + + // --- STEP 2: REGISTRAZIONE PRIMO NEGOZIO --- + Future saveStore(StoreModel store) async { + if (state.companyId == null) return; + + emit(state.copyWith(isLoading: true)); + try { + // Iniettiamo forzatamente il companyId ottenuto dallo step precedente + final storeToSave = store.copyWith(companyId: state.companyId); + final savedStore = await _repository.createStore(storeToSave); + + emit( + state.copyWith( + isLoading: false, + step: OnboardingStep.staff, + storeId: savedStore.id, + ), + ); + } catch (e) { + emit( + state.copyWith(isLoading: false, error: "Errore salvataggio store: $e"), + ); + } + } + + // --- STEP 3: REGISTRAZIONE PROFILO STAFF (PAZIENTE ZERO) --- + Future saveStaff(StaffMemberModel staff) async { + if (state.companyId == null || state.storeId == null) return; + + emit(state.copyWith(isLoading: true)); + try { + // PARANOIA MODE: Forziamo i legami e il ruolo di sistema 'admin' + final staffToSave = staff.copyWith( + companyId: state.companyId!, + storeId: state.storeId!, + userId: _sessionCubit.state.user!.id, // Dall'utente loggato in Supabase + systemRole: SystemRole.admin, // Blindato! + ); + + await _repository.createStaffMember(staffToSave); + + emit(state.copyWith(isLoading: false, step: OnboardingStep.completed)); + + // Svegliamo il SessionCubit: lui ricalcolerà tutto e aprirà la Dashboard + await _sessionCubit.initializeSession(); + } catch (e) { + emit( + state.copyWith(isLoading: false, error: "Errore creazione profilo: $e"), + ); + } + } +} diff --git a/lib/features/onboarding/blocs/onboarding_state.dart b/lib/features/onboarding/blocs/onboarding_state.dart new file mode 100644 index 0000000..f28fa17 --- /dev/null +++ b/lib/features/onboarding/blocs/onboarding_state.dart @@ -0,0 +1,37 @@ +import 'package:equatable/equatable.dart'; +import 'package:flux/core/blocs/session/session_cubit.dart'; + +class OnboardingState extends Equatable { + final OnboardingStep step; + final bool isLoading; + final String? error; + final String? companyId; // Salvato dopo lo Step 1 + final String? storeId; // Salvato dopo lo Step 2 + + const OnboardingState({ + required this.step, + this.isLoading = false, + this.error, + this.companyId, + this.storeId, + }); + + OnboardingState copyWith({ + OnboardingStep? step, + bool? isLoading, + String? error, + String? companyId, + String? storeId, + }) { + return OnboardingState( + step: step ?? this.step, + isLoading: isLoading ?? this.isLoading, + error: error, // Se non passato, resettiamo l'errore + companyId: companyId ?? this.companyId, + storeId: storeId ?? this.storeId, + ); + } + + @override + List get props => [step, isLoading, error, companyId, storeId]; +} diff --git a/lib/features/onboarding/ui/onboarding_screen.dart b/lib/features/onboarding/ui/onboarding_screen.dart new file mode 100644 index 0000000..bc26faa --- /dev/null +++ b/lib/features/onboarding/ui/onboarding_screen.dart @@ -0,0 +1,355 @@ +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/company/models/company_model.dart'; +import 'package:flux/features/master_data/store/models/store_model.dart'; +import 'package:flux/features/master_data/staff/models/staff_member_model.dart'; +import 'package:flux/features/onboarding/blocs/onboarding_cubit.dart'; + +// Sostituisci con il percorso corretto della tua FluxTextField +import 'package:flux/core/widgets/flux_text_field.dart'; +import 'package:flux/features/onboarding/blocs/onboarding_state.dart'; + +class OnboardingScreen extends StatefulWidget { + const OnboardingScreen({super.key}); + + @override + State createState() => _OnboardingScreenState(); +} + +class _OnboardingScreenState extends State { + late PageController _pageController; + + // --- CHIAVI DEI FORM (Per la validazione indipendente di ogni step) --- + final _companyFormKey = GlobalKey(); + final _storeFormKey = GlobalKey(); + final _staffFormKey = GlobalKey(); + + // --- CONTROLLERS: STEP 1 (Company) --- + final _companyNameCtrl = TextEditingController(); + final _companyVatCtrl = TextEditingController(); + + // --- CONTROLLERS: STEP 2 (Store) --- + final _storeNameCtrl = TextEditingController(); + final _storeAddressCtrl = TextEditingController(); + + // --- CONTROLLERS: STEP 3 (Staff) --- + final _staffFirstNameCtrl = TextEditingController(); + final _staffLastNameCtrl = TextEditingController(); + final _staffJobTitleCtrl = TextEditingController(); + + @override + void initState() { + super.initState(); + // Calcoliamo la pagina iniziale in base allo step salvato nel Cubit + final initialStep = context.read().state.step; + _pageController = PageController(initialPage: _getPageIndex(initialStep)); + } + + @override + void dispose() { + _pageController.dispose(); + _companyNameCtrl.dispose(); + _companyVatCtrl.dispose(); + _storeNameCtrl.dispose(); + _storeAddressCtrl.dispose(); + _staffFirstNameCtrl.dispose(); + _staffLastNameCtrl.dispose(); + _staffJobTitleCtrl.dispose(); + super.dispose(); + } + + int _getPageIndex(OnboardingStep step) { + switch (step) { + case OnboardingStep.company: + return 0; + case OnboardingStep.store: + return 1; + case OnboardingStep.staff: + return 2; + default: + return 0; + } + } + + // Validatore generico riutilizzabile + String? _requireValidator(String? value) { + if (value == null || value.trim().isEmpty) { + return 'Campo obbligatorio'; + } + return null; + } + + @override + Widget build(BuildContext context) { + return BlocConsumer( + // Ascoltiamo i cambi di stato per animare la pagina e mostrare errori + listenWhen: (previous, current) => + previous.step != current.step || previous.error != current.error, + listener: (context, state) { + // Gestione Errori + if (state.error != null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(state.error!), backgroundColor: Colors.red), + ); + } + + // Se ha finito, non animiamo nulla: il GoRouter prenderà il controllo a breve! + if (state.step == OnboardingStep.completed) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text("Configurazione completata! Benvenuto a bordo 🚀"), + backgroundColor: Colors.green, + ), + ); + return; + } + + // Animazione cambio pagina + if (state.step != OnboardingStep.completed) { + final targetPage = _getPageIndex(state.step); + if (_pageController.hasClients && + _pageController.page?.round() != targetPage) { + _pageController.animateToPage( + targetPage, + duration: const Duration(milliseconds: 600), + curve: Curves.easeInOutCubic, // Animazione super fluida + ); + } + } + }, + builder: (context, state) { + return Scaffold( + body: SafeArea( + child: Stack( + children: [ + // IL PAGEVIEW CORAZZATO + PageView( + controller: _pageController, + physics: + const NeverScrollableScrollPhysics(), // Vietato lo swipe manuale! + children: [ + _buildCompanyForm(context, state), + _buildStoreForm(context, state), + _buildStaffForm(context, state), + ], + ), + + // OVERLAY CARICAMENTO + if (state.isLoading) + Container( + color: Colors.black.withValues(alpha: 0.4), + child: const Center(child: CircularProgressIndicator()), + ), + ], + ), + ), + ); + }, + ); + } + + // ========================================================================= + // SCHERMATE DEI SINGOLI STEP + // ========================================================================= + + Widget _buildCompanyForm(BuildContext context, OnboardingState state) { + return Padding( + padding: const EdgeInsets.all(32.0), + child: Form( + key: _companyFormKey, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Text( + "Iniziamo! 🏢", + style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + const Text( + "Inserisci i dati della tua attività per configurare il tuo ambiente FLUX.", + style: TextStyle(fontSize: 16, color: Colors.grey), + ), + const SizedBox(height: 48), + + FluxTextField( + label: 'Ragione Sociale / Nome Azienda', + controller: _companyNameCtrl, + validator: _requireValidator, + ), + const SizedBox(height: 16), + FluxTextField( + label: 'Partita IVA', + controller: _companyVatCtrl, + validator: _requireValidator, + ), + + const Spacer(), + ElevatedButton( + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + onPressed: () { + if (_companyFormKey.currentState!.validate()) { + // MOCK DI ESEMPIO: Sostituisci con il tuo vero CompanyModel + final newCompany = CompanyModel( + ownerId: '', // Questo lo gestirà o ignorerà il Cubit + name: _companyNameCtrl.text.trim(), + vatNumber: _companyVatCtrl.text.trim(), + ); + context.read().saveCompany(newCompany); + } + }, + child: const Text( + "Salva e Prosegui", + style: TextStyle(fontSize: 16), + ), + ), + const SizedBox(height: 16), + ], + ), + ), + ); + } + + Widget _buildStoreForm(BuildContext context, OnboardingState state) { + return Padding( + padding: const EdgeInsets.all(32.0), + child: Form( + key: _storeFormKey, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Text( + "Il tuo Negozio 🏪", + style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + const Text( + "Dove si trova il tuo punto vendita principale? (Potrai aggiungerne altri in seguito).", + style: TextStyle(fontSize: 16, color: Colors.grey), + ), + const SizedBox(height: 48), + + FluxTextField( + label: 'Nome Negozio (es. Sede Centrale)', + controller: _storeNameCtrl, + validator: _requireValidator, + ), + const SizedBox(height: 16), + FluxTextField( + label: 'Indirizzo completo', + controller: _storeAddressCtrl, + validator: _requireValidator, + ), + + const Spacer(), + ElevatedButton( + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + onPressed: () { + if (_storeFormKey.currentState!.validate()) { + final newStore = StoreModel( + companyId: '', // Iniettato dal Cubit + name: _storeNameCtrl.text.trim(), + address: _storeAddressCtrl.text.trim(), + isActive: true, + ); + context.read().saveStore(newStore); + } + }, + child: const Text( + "Salva Negozio", + style: TextStyle(fontSize: 16), + ), + ), + const SizedBox(height: 16), + ], + ), + ), + ); + } + + Widget _buildStaffForm(BuildContext context, OnboardingState state) { + return Padding( + padding: const EdgeInsets.all(32.0), + child: Form( + key: _staffFormKey, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Text( + "Il tuo Profilo 👤", + style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + const Text( + "Ultimo step! Crea il tuo profilo operativo per iniziare a usare FLUX.", + style: TextStyle(fontSize: 16, color: Colors.grey), + ), + const SizedBox(height: 48), + + FluxTextField( + label: 'Nome', + controller: _staffFirstNameCtrl, + validator: _requireValidator, + ), + const SizedBox(height: 16), + FluxTextField( + label: 'Cognome', + controller: _staffLastNameCtrl, + validator: _requireValidator, + ), + const SizedBox(height: 16), + FluxTextField( + label: 'Etichetta Ruolo (es. Titolare, Manager)', + controller: _staffJobTitleCtrl, + // Il jobTitle può anche essere opzionale, decidi tu! + ), + + const Spacer(), + ElevatedButton( + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + backgroundColor: Colors.black, // O il tuo context.accent + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + onPressed: () { + if (_staffFormKey.currentState!.validate()) { + final newStaff = StaffMemberModel( + companyId: '', // Iniettato dal Cubit + storeId: '', // Iniettato dal Cubit + userId: '', // Iniettato dal Cubit + name: _staffFirstNameCtrl.text.trim(), + surname: _staffLastNameCtrl.text.trim(), + jobTitle: _staffJobTitleCtrl.text.trim(), + // systemRole non viene passato qui: la Paranoia Mode del Cubit forzerà "admin" + ); + context.read().saveStaff(newStaff); + } + }, + child: const Text( + "Entra in FLUX", + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + ), + const SizedBox(height: 16), + ], + ), + ), + ); + } +} diff --git a/lib/features/onboarding/ui/onboarding_view.dart b/lib/features/onboarding/ui/onboarding_view.dart new file mode 100644 index 0000000..94223c9 --- /dev/null +++ b/lib/features/onboarding/ui/onboarding_view.dart @@ -0,0 +1,211 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flux/core/blocs/session/session_cubit.dart'; +import 'package:flux/core/widgets/flux_text_field.dart'; +import 'package:flux/features/onboarding/blocs/onboarding_cubit.dart'; +import 'package:flux/features/onboarding/blocs/onboarding_state.dart'; +// Importa i tuoi file (cubit, modelli, ecc.) + +class OnboardingScreen extends StatefulWidget { + const OnboardingScreen({super.key}); + + @override + State createState() => _OnboardingScreenState(); +} + +class _OnboardingScreenState extends State { + late PageController _pageController; + + @override + void initState() { + super.initState(); + // Inizializziamo il controller sulla pagina giusta. + // L'indice parte da 0. company=0, store=1, staff=2. + final initialStep = context.read().state.step; + _pageController = PageController(initialPage: _getPageIndex(initialStep)); + } + + @override + void dispose() { + _pageController.dispose(); + super.dispose(); + } + + int _getPageIndex(OnboardingStep step) { + switch (step) { + case OnboardingStep.company: + return 0; + case OnboardingStep.store: + return 1; + case OnboardingStep.staff: + return 2; + default: + return 0; + } + } + + @override + Widget build(BuildContext context) { + return BlocConsumer( + // Ascoltiamo solo quando cambia lo step per animare la pagina + listenWhen: (previous, current) => previous.step != current.step, + listener: (context, state) { + if (state.step == OnboardingStep.completed) { + // Il SessionCubit prenderà il controllo e farà il redirect, + // qui potremmo mostrare un bel toast di successo. + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Configurazione completata! 🚀")), + ); + return; + } + + final targetPage = _getPageIndex(state.step); + _pageController.animateToPage( + targetPage, + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOut, + ); + }, + builder: (context, state) { + return Scaffold( + body: SafeArea( + child: Stack( + children: [ + // Il PageView disabilita lo scorrimento manuale con NeverScrollableScrollPhysics + PageView( + controller: _pageController, + physics: const NeverScrollableScrollPhysics(), + children: [ + _buildCompanyForm(context, state), + _buildStoreForm(context, state), + _buildStaffForm( + context, + state, + ), // Qui c'è la magia paranoica + ], + ), + + // Overlay di caricamento universale + if (state.isLoading) + Container( + color: Colors.black.withValues(alpha: 0.5), + child: const Center(child: CircularProgressIndicator()), + ), + ], + ), + ), + ); + }, + ); + } + + // --- I METODI DEI FORM --- + // (Nella realtà li metterai in file separati o widget custom per pulizia) + + Widget _buildCompanyForm(BuildContext context, OnboardingState state) { + // Controller e chiavi del form... + return Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Text( + "Step 1: La tua Azienda", + style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + const Text("Inserisci i dati della tua attività."), + const SizedBox(height: 32), + + // Esempio usando la tua FluxTextField + FluxTextField(label: 'Ragione Sociale / Nome Azienda'), + const SizedBox(height: 16), + FluxTextField(label: 'Partita IVA'), + + const Spacer(), + ElevatedButton( + onPressed: () { + // 1. Valida il form + // 2. Crea il CompanyModel + // 3. Chiama il Cubit: + // context.read().saveCompany(newCompany); + }, + child: const Text("Salva e prosegui"), + ), + ], + ), + ); + } + + Widget _buildStoreForm(BuildContext context, OnboardingState state) { + return Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Text( + "Step 2: Il primo Negozio", + style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + const Text("Dove si trova il tuo punto vendita principale?"), + const SizedBox(height: 32), + + FluxTextField(label: 'Nome Negozio (es. Sede Centrale)'), + const SizedBox(height: 16), + FluxTextField(label: 'Indirizzo'), + + const Spacer(), + ElevatedButton( + onPressed: () { + // context.read().saveStore(newStore); + }, + child: const Text("Salva Negozio"), + ), + ], + ), + ); + } + + Widget _buildStaffForm(BuildContext context, OnboardingState state) { + // NOTA PARANOICA: Qui chiediamo jobTitle (Testo libero), + // ma NON diamo modo di scegliere il system_role! + return Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Text( + "Step 3: Il tuo Profilo", + style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + const Text("Crea il tuo profilo operativo per iniziare a lavorare."), + const SizedBox(height: 32), + + FluxTextField(label: 'Nome'), + const SizedBox(height: 16), + FluxTextField(label: 'Cognome'), + const SizedBox(height: 16), + + // TESTO LIBERO! Il cliente ci scrive quello che vuole ("CEO", "Boss", "Stagista") + FluxTextField(label: 'Ruolo / Etichetta (es. Titolare)'), + + const Spacer(), + ElevatedButton( + onPressed: () { + // Quando chiami il Cubit, passi i dati della UI. + // Ti ricordi? L'OnboardingCubit forzerà `system_role = SystemRole.admin` + // dietro le quinte! + // context.read().saveStaff(newStaff); + }, + child: const Text("Inizia a usare Flux!"), + ), + ], + ), + ); + } +} diff --git a/lib/features/services/blocs/services_cubit.dart b/lib/features/services/blocs/services_cubit.dart index db154bd..ddc82e8 100644 --- a/lib/features/services/blocs/services_cubit.dart +++ b/lib/features/services/blocs/services_cubit.dart @@ -2,7 +2,7 @@ import 'package:equatable/equatable.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/core/blocs/session/session_bloc.dart'; +import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/utils/string_extensions.dart'; import 'package:flux/features/services/data/services_repository.dart'; import 'package:flux/features/services/models/energy_service_model.dart'; diff --git a/lib/features/services/data/services_repository.dart b/lib/features/services/data/services_repository.dart index f81f417..52fa788 100644 --- a/lib/features/services/data/services_repository.dart +++ b/lib/features/services/data/services_repository.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:flux/core/blocs/session/session_bloc.dart'; +import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/features/customers/data/customer_repository.dart'; import 'package:flux/features/customers/models/customer_file_model.dart'; import 'package:flux/features/services/models/service_file_model.dart'; diff --git a/lib/features/services/ui/service_form_screen/entertainment_service_card.dart b/lib/features/services/ui/service_form_screen/entertainment_service_card.dart index 9243a96..e7fe8ab 100644 --- a/lib/features/services/ui/service_form_screen/entertainment_service_card.dart +++ b/lib/features/services/ui/service_form_screen/entertainment_service_card.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/core/blocs/session/session_bloc.dart'; +import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/features/master_data/providers/blocs/provider_cubit.dart'; import 'package:flux/features/master_data/providers/models/provider_model.dart'; import 'package:flux/features/services/data/services_repository.dart'; diff --git a/lib/features/services/utils/service_actions.dart b/lib/features/services/utils/service_actions.dart index 4a51388..5f826b6 100644 --- a/lib/features/services/utils/service_actions.dart +++ b/lib/features/services/utils/service_actions.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/core/blocs/session/session_bloc.dart'; +import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/features/master_data/store/bloc/store_cubit.dart'; import 'package:flux/features/services/blocs/services_cubit.dart'; import 'package:flux/features/services/models/service_model.dart'; diff --git a/lib/main.dart b/lib/main.dart index 9b0ff8b..e633335 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,13 +1,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:flux/core/blocs/session/session_bloc.dart'; +import 'package:get_it/get_it.dart'; +import 'package:go_router/go_router.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; +import 'package:flux/core/blocs/session/session_cubit.dart'; +import 'package:flux/core/data/core_repository.dart'; import 'package:flux/core/routes/app_router.dart'; import 'package:flux/core/theme/theme.dart'; import 'package:flux/core/theme/bloc/theme_bloc.dart'; -import 'package:flux/features/auth/bloc/auth_bloc.dart'; -import 'package:flux/features/company/bloc/company_bloc.dart'; -import 'package:flux/features/company/data/company_repository.dart'; import 'package:flux/features/customers/blocs/customer_cubit.dart'; import 'package:flux/features/customers/data/customer_repository.dart'; import 'package:flux/features/master_data/products/blocs/product_cubit.dart'; @@ -21,14 +23,12 @@ import 'package:flux/features/master_data/store/data/store_repository.dart'; import 'package:flux/features/services/blocs/services_cubit.dart'; import 'package:flux/features/services/data/services_repository.dart'; import 'package:flux/features/settings/settings.dart'; -import 'package:get_it/get_it.dart'; -import 'package:go_router/go_router.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await dotenv.load(fileName: ".env"); + + // Inizializza le dipendenze PRIMA di lanciare l'app await setupLocator(); runApp( @@ -37,17 +37,16 @@ void main() async { BlocProvider( create: (context) => ThemeBloc()..add(LoadThemeEvent()), ), - BlocProvider(create: (_) => GetIt.I()), - BlocProvider(create: (_) => AuthBloc()), - BlocProvider(create: (_) => CompanyBloc()), - BlocProvider(create: (_) => StoreCubit()..loadStores()), + // Il Vigile Urbano viene inizializzato! + BlocProvider(create: (_) => GetIt.I()), + + // Cubit delle feature + BlocProvider(create: (_) => StoreCubit()), BlocProvider(create: (_) => CustomerCubit()), BlocProvider(create: (_) => ProductCubit()), - BlocProvider(create: (_) => StaffCubit()..loadAllStaff()), + BlocProvider(create: (_) => StaffCubit()), BlocProvider(create: (_) => ServicesCubit()), - BlocProvider( - create: (_) => ProvidersCubit()..loadProviders(null), - ), + BlocProvider(create: (_) => ProvidersCubit()), ], child: const FluxApp(), ), @@ -56,6 +55,7 @@ void main() async { Future setupLocator() async { final GetIt getIt = GetIt.instance; + getIt.registerSingleton( await SharedPreferences.getInstance(), ); @@ -65,15 +65,30 @@ Future setupLocator() async { anonKey: dotenv.env['SUPABASE_ANON_KEY'] ?? '', ); getIt.registerSingleton(Supabase.instance.client); + + // Settings getIt.registerLazySingleton(() => AppSettings()); - getIt.registerLazySingleton(() => CompanyRepository()); + + // Repositories + getIt.registerLazySingleton( + () => CoreRepository(), + ); // <-- NUOVO getIt.registerLazySingleton(() => StoreRepository()); getIt.registerLazySingleton(() => CustomerRepository()); getIt.registerLazySingleton(() => ProductRepository()); getIt.registerLazySingleton(() => StaffRepository()); getIt.registerLazySingleton(() => ServicesRepository()); getIt.registerLazySingleton(() => ProviderRepository()); - getIt.registerSingleton(SessionBloc()..add(AppStarted())); + + // NOTA: CompanyRepository l'ho tolto perché la logica della Company + // ora è gestita dal CoreRepository durante l'Onboarding. + // Se ti serve per altro, rimettilo pure! + + // Inizializziamo il SessionCubit (che prende CoreRepository e SharedPreferences) + // Usiamo registerSingleton così viene creato subito e inizia ad ascoltare Supabase Auth. + getIt.registerSingleton( + SessionCubit(getIt(), getIt()), + ); } class FluxApp extends StatefulWidget { @@ -89,26 +104,28 @@ class _FluxAppState extends State { @override void initState() { super.initState(); - // Lo creiamo una volta sola all'avvio dell'app - _router = AppRouter.createRouter(context.read()); + // Creiamo il router passandogli il Cubit per i redirect + _router = AppRouter.createRouter(context.read()); } @override Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - if (state.status == SessionStatus.unknown) { + return BlocBuilder( + builder: (context, sessionState) { + // Usa l'enum corretto (initial, non unknown) + if (sessionState.status == SessionStatus.initial) { return _buildLoadingScreen(); } + return BlocBuilder( - builder: (context, state) { + builder: (context, themeState) { return MaterialApp.router( title: 'FLUX Gestionale', debugShowCheckedModeBanner: false, theme: fluxLightTheme, darkTheme: fluxDarkTheme, - themeMode: state.currentTheme.themeMode, - routerConfig: _router, // Usa l'istanza mantenuta nello stato + themeMode: themeState.currentTheme.themeMode, + routerConfig: _router, ); }, ); @@ -116,7 +133,6 @@ class _FluxAppState extends State { ); } - // Una semplice schermata di caricamento coerente con il brand Widget _buildLoadingScreen() { return MaterialApp( debugShowCheckedModeBanner: false, @@ -125,7 +141,6 @@ class _FluxAppState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - // Qui puoi mettere il tuo logo const Icon(Icons.bolt, size: 64, color: Colors.blue), const SizedBox(height: 24), const CircularProgressIndicator(), -- 2.43.0 From 09398a1b342bfe3a368978f548768f33dabad663 Mon Sep 17 00:00:00 2001 From: Mark M2 Macbook Date: Tue, 21 Apr 2026 11:26:42 +0200 Subject: [PATCH 2/6] pennellato git da rosso ad arancio e verde --- lib/core/blocs/session/session_cubit.dart | 2 +- lib/core/routes/app_router.dart | 8 +- lib/core/utils/validators.dart | 6 + .../company/models/company_model.dart | 16 ++ .../company/ui/create_company_screen.dart | 6 +- .../customers/blocs/customer_cubit.dart | 8 +- .../customers/data/customer_repository.dart | 2 +- .../customers/ui/customers_content.dart | 6 +- lib/features/home/ui/dashboard_content.dart | 4 +- lib/features/home/ui/home_screen.dart | 8 +- .../products/blocs/product_cubit.dart | 11 +- .../providers/blocs/provider_cubit.dart | 6 +- .../master_data/staff/blocs/staff_cubit.dart | 4 +- .../staff/models/staff_member_model.dart | 60 +++-- .../master_data/staff/ui/staff_screen.dart | 23 +- .../master_data/store/bloc/store_cubit.dart | 4 +- .../master_data/store/models/store_model.dart | 11 + .../store/ui/create_store_screen.dart | 14 +- .../master_data/store/ui/store_form.dart | 4 +- .../onboarding/blocs/onboarding_cubit.dart | 1 - .../ui/company_onboarding_form.dart | 83 +++++++ .../onboarding/ui/onboarding_screen.dart | 110 +-------- .../onboarding/ui/onboarding_view.dart | 211 ------------------ .../services/blocs/services_cubit.dart | 8 +- .../services/data/services_repository.dart | 2 +- .../entertainment_service_card.dart | 2 +- .../services/utils/service_actions.dart | 6 +- 27 files changed, 237 insertions(+), 389 deletions(-) create mode 100644 lib/core/utils/validators.dart create mode 100644 lib/features/onboarding/ui/company_onboarding_form.dart delete mode 100644 lib/features/onboarding/ui/onboarding_view.dart diff --git a/lib/core/blocs/session/session_cubit.dart b/lib/core/blocs/session/session_cubit.dart index f476c2c..d3d7d8f 100644 --- a/lib/core/blocs/session/session_cubit.dart +++ b/lib/core/blocs/session/session_cubit.dart @@ -51,7 +51,7 @@ class SessionCubit extends Cubit { } // 2. Controllo Negozi - final stores = await _repository.getStoresByCompanyId(company.id); + final stores = await _repository.getStoresByCompanyId(company.id!); if (stores.isEmpty) { return emit( state.copyWith( diff --git a/lib/core/routes/app_router.dart b/lib/core/routes/app_router.dart index 4dc2953..dd59f98 100644 --- a/lib/core/routes/app_router.dart +++ b/lib/core/routes/app_router.dart @@ -1,8 +1,11 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flux/features/auth/ui/auth_screen.dart'; import 'package:flux/features/customers/models/customer_model.dart'; import 'package:flux/features/customers/ui/customer_detail_screen.dart'; +import 'package:flux/features/home/ui/home_screen.dart'; import 'package:flux/features/master_data/products/ui/products_screen.dart'; +import 'package:flux/features/onboarding/ui/onboarding_screen.dart'; import 'package:flux/features/services/models/service_model.dart'; import 'package:flux/features/services/ui/service_form_screen/service_form_screen.dart'; import 'package:go_router/go_router.dart'; @@ -58,7 +61,8 @@ class AppRouter { routes: [ GoRoute( path: '/login', - builder: (context, state) => const LoginScreen(), + //builder: (context, state) => const LoginScreen(), + builder: (context, state) => const AuthScreen(), ), GoRoute( path: '/onboarding', @@ -69,7 +73,7 @@ class AppRouter { ), GoRoute( path: '/', - builder: (context, state) => const DashboardScreen(), // La tua home + builder: (context, state) => const HomeScreen(), // La tua home ), GoRoute( path: '/customer/:id', diff --git a/lib/core/utils/validators.dart b/lib/core/utils/validators.dart new file mode 100644 index 0000000..c689a60 --- /dev/null +++ b/lib/core/utils/validators.dart @@ -0,0 +1,6 @@ +String? notEmptyValidator(String? value) { + if (value == null || value.trim().isEmpty) { + return 'Campo obbligatorio'; + } + return null; +} diff --git a/lib/features/company/models/company_model.dart b/lib/features/company/models/company_model.dart index 65406af..7cc8013 100644 --- a/lib/features/company/models/company_model.dart +++ b/lib/features/company/models/company_model.dart @@ -132,6 +132,22 @@ class CompanyModel extends Equatable { ); } + factory CompanyModel.empty() { + return const CompanyModel( + id: null, + createdAt: null, + userId: '', + ragioneSociale: '', + indirizzo: '', + cap: '', + citta: '', + provincia: '', + partitaIva: '', + codiceFiscale: '', + codiceUnivoco: '', + ); + } + factory CompanyModel.fromMap(Map map) { return CompanyModel( id: map['id'] as String?, diff --git a/lib/features/company/ui/create_company_screen.dart b/lib/features/company/ui/create_company_screen.dart index fa2ecf6..0127f0f 100644 --- a/lib/features/company/ui/create_company_screen.dart +++ b/lib/features/company/ui/create_company_screen.dart @@ -46,7 +46,7 @@ class _CreateCompanyScreenState extends State { void _onSave() { if (_formKey.currentState!.validate()) { // Recuperiamo l'ID utente attuale da Supabase o dal SessionBloc - final userId = context.read().state.userId!; + final userId = context.read().state.user!.id; final company = CompanyModel( userId: userId, @@ -77,7 +77,7 @@ class _CreateCompanyScreenState extends State { onPressed: () { // Qui chiami il tuo Bloc dell'autenticazione per fare logout // Esempio se hai un AuthBloc o SessionBloc: - context.read().add(LogoutRequested()); + //context.read().add(LogoutRequested()); // Se vuoi solo tornare brutalmente alla login per testare il logo: // Navigator.of(context).pushReplacementNamed('/login'); @@ -92,7 +92,7 @@ class _CreateCompanyScreenState extends State { //GetIt.I.get().setCurrentCompany(state.company); // 2. Notifichiamo il SessionBloc per cambiare pagina - context.read().add(AppStarted()); + //context.read().add(AppStarted()); } if (state.status == CompanyStatus.failure) { diff --git a/lib/features/customers/blocs/customer_cubit.dart b/lib/features/customers/blocs/customer_cubit.dart index 248913c..64f4882 100644 --- a/lib/features/customers/blocs/customer_cubit.dart +++ b/lib/features/customers/blocs/customer_cubit.dart @@ -10,7 +10,7 @@ part 'customer_state.dart'; class CustomerCubit extends Cubit { final CustomerRepository _repository = GetIt.I(); - final SessionBloc _sessionBloc = GetIt.I(); + final SessionCubit _sessionCubit = GetIt.I(); // Variabile per gestire il debounce della ricerca Timer? _searchDebounce; @@ -22,7 +22,7 @@ class CustomerCubit extends Cubit { emit(state.copyWith(status: CustomerStatus.loading)); try { final customers = await _repository.getCustomers( - _sessionBloc.state.company!.id, + _sessionCubit.state.company!.id!, ); emit( state.copyWith(status: CustomerStatus.success, customers: customers), @@ -111,7 +111,7 @@ class CustomerCubit extends Cubit { // Nessun "loading" state qui, per evitare sfarfallii visivi mentre si scrive try { final results = await _repository.searchCustomers( - _sessionBloc.state.company!.id, + _sessionCubit.state.company!.id!, query, ); emit( @@ -137,7 +137,7 @@ class CustomerCubit extends Cubit { nome: name, telefono: phone ?? '', email: email ?? '', - companyId: _sessionBloc.state.company!.id, + companyId: _sessionCubit.state.company!.id!, note: '', ); diff --git a/lib/features/customers/data/customer_repository.dart b/lib/features/customers/data/customer_repository.dart index 463226e..6b3f3c2 100644 --- a/lib/features/customers/data/customer_repository.dart +++ b/lib/features/customers/data/customer_repository.dart @@ -8,7 +8,7 @@ import '../models/customer_model.dart'; class CustomerRepository { final SupabaseClient _supabase = GetIt.I(); - final String companyId = GetIt.I.get().state.company!.id; + final String companyId = GetIt.I.get().state.company!.id!; // Crea un nuovo cliente Future saveCustomer(CustomerModel customer) async { diff --git a/lib/features/customers/ui/customers_content.dart b/lib/features/customers/ui/customers_content.dart index 6f46905..d9eb0b1 100644 --- a/lib/features/customers/ui/customers_content.dart +++ b/lib/features/customers/ui/customers_content.dart @@ -24,14 +24,14 @@ class _CustomersContentState extends State { } void _loadInitialCustomers() { - final companyId = context.read().state.company?.id; + final companyId = context.read().state.company?.id; if (companyId != null) { context.read().loadCustomers(); } } void _onSearch(String query) { - final companyId = context.read().state.company?.id; + final companyId = context.read().state.company?.id; if (companyId != null) { context.read().searchCustomers(query); } @@ -48,7 +48,7 @@ class _CustomersContentState extends State { child: CustomerForm( customer: customer, onSave: (customerFromForm) { - final session = context.read().state; + final session = context.read().state; final companyId = session.company?.id; if (companyId == null) return; diff --git a/lib/features/home/ui/dashboard_content.dart b/lib/features/home/ui/dashboard_content.dart index 3c82a27..d32b204 100644 --- a/lib/features/home/ui/dashboard_content.dart +++ b/lib/features/home/ui/dashboard_content.dart @@ -16,9 +16,9 @@ class DashboardContent extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocBuilder( + return BlocBuilder( builder: (context, state) { - final store = state.selectedStore; + final store = state.currentStore; final company = state.company; return Scaffold( diff --git a/lib/features/home/ui/home_screen.dart b/lib/features/home/ui/home_screen.dart index d0f3c39..59dc14e 100644 --- a/lib/features/home/ui/home_screen.dart +++ b/lib/features/home/ui/home_screen.dart @@ -30,7 +30,7 @@ class _HomeScreenState extends State { @override Widget build(BuildContext context) { - return BlocBuilder( + return BlocBuilder( builder: (context, state) { return LayoutBuilder( builder: (context, constraints) { @@ -203,7 +203,7 @@ class _HomeScreenState extends State { ), const SizedBox(width: 12), Text( - GetIt.I.get().state.company?.ragioneSociale ?? + GetIt.I.get().state.company?.ragioneSociale ?? "Utente", style: TextStyle( fontWeight: FontWeight.bold, @@ -246,9 +246,9 @@ class _HomeScreenState extends State { ), onPressed: () { Navigator.pop(dialogContext); // Chiude la Dialog - context.read().add( + /* context.read().add( LogoutRequested(), - ); // Esegue il logout + ); // Esegue il logout */ }, child: const Text("Esci"), ), diff --git a/lib/features/master_data/products/blocs/product_cubit.dart b/lib/features/master_data/products/blocs/product_cubit.dart index 973dd35..b35a6bc 100644 --- a/lib/features/master_data/products/blocs/product_cubit.dart +++ b/lib/features/master_data/products/blocs/product_cubit.dart @@ -11,7 +11,7 @@ part 'product_state.dart'; class ProductCubit extends Cubit { final ProductRepository _repository = GetIt.I(); - final SessionBloc _sessionBloc = GetIt.I(); + final SessionCubit _sessionCubit = GetIt.I(); ProductCubit() : super(const ProductState()); @@ -20,7 +20,7 @@ class ProductCubit extends Cubit { emit(state.copyWith(status: ProductStatus.loading)); try { final brands = await _repository.getBrands( - _sessionBloc.state.company!.id, + _sessionCubit.state.company!.id!, ); emit(state.copyWith(status: ProductStatus.success, brands: brands)); } catch (e) { @@ -54,7 +54,7 @@ class ProductCubit extends Cubit { final brand = BrandModel( id: id, name: name, - companyId: _sessionBloc.state.company!.id, + companyId: _sessionCubit.state.company!.id!, ); final newBrand = await _repository.upsertBrand(brand); await loadBrands(); // Ricarichiamo la lista aggiornata @@ -137,7 +137,10 @@ class ProductCubit extends Cubit { // 1. Cerchiamo o creiamo il Brand // (Usa una funzione upsert o una ricerca rapida nel repository) brand ??= await _repository.upsertBrand( - BrandModel(name: brandName, companyId: _sessionBloc.state.company!.id), + BrandModel( + name: brandName, + companyId: _sessionCubit.state.company!.id!, + ), ); // 2. Creiamo il Modello legato al Brand diff --git a/lib/features/master_data/providers/blocs/provider_cubit.dart b/lib/features/master_data/providers/blocs/provider_cubit.dart index 857c932..8ca498c 100644 --- a/lib/features/master_data/providers/blocs/provider_cubit.dart +++ b/lib/features/master_data/providers/blocs/provider_cubit.dart @@ -52,7 +52,7 @@ class ProvidersState extends Equatable { class ProvidersCubit extends Cubit { final ProviderRepository _repository = GetIt.I(); - final SessionBloc _sessionBloc = GetIt.I(); + final SessionCubit _sessionCubit = GetIt.I(); ProvidersCubit() : super(const ProvidersState()); @@ -61,7 +61,7 @@ class ProvidersCubit extends Cubit { emit(state.copyWith(isLoading: true)); try { final all = await _repository.fetchAllCompanyProviders( - _sessionBloc.state.company!.id, + _sessionCubit.state.company!.id!, ); List associated = []; @@ -135,7 +135,7 @@ class ProvidersCubit extends Cubit { ) async { emit(state.copyWith(isLoading: true)); // Assicuriamoci di settare la companyId prima di salvare - provider = provider.copyWith(companyId: _sessionBloc.state.company!.id); + provider = provider.copyWith(companyId: _sessionCubit.state.company!.id); try { // 1. Salviamo l'anagrafica (upsert) // Se è un nuovo provider, l'ID potrebbe essere generato qui dal DB diff --git a/lib/features/master_data/staff/blocs/staff_cubit.dart b/lib/features/master_data/staff/blocs/staff_cubit.dart index 4c3fb60..9c37a93 100644 --- a/lib/features/master_data/staff/blocs/staff_cubit.dart +++ b/lib/features/master_data/staff/blocs/staff_cubit.dart @@ -10,7 +10,7 @@ part 'staff_state.dart'; class StaffCubit extends Cubit { final StaffRepository _repository = GetIt.I.get(); - final SessionBloc _sessionBloc = GetIt.I(); + final SessionCubit _sessionCubit = GetIt.I(); StaffCubit() : super(const StaffState()); @@ -19,7 +19,7 @@ class StaffCubit extends Cubit { emit(state.copyWith(isLoading: true, error: null)); try { final staff = await _repository.getStaffMembers( - _sessionBloc.state.company!.id, + _sessionCubit.state.company!.id!, ); final Map> storesByStaff = {}; for (StaffMemberModel member in staff) { diff --git a/lib/features/master_data/staff/models/staff_member_model.dart b/lib/features/master_data/staff/models/staff_member_model.dart index 3f8a1c5..1368716 100644 --- a/lib/features/master_data/staff/models/staff_member_model.dart +++ b/lib/features/master_data/staff/models/staff_member_model.dart @@ -18,44 +18,61 @@ enum SystemRole { class StaffMemberModel extends Equatable { final String? id; final String companyId; - final String storeId; final String userId; final String name; - final String surname; - final String? - jobTitle; // Testo libero! Il cliente ci scrive quello che vuole. - final SystemRole systemRole; // ENUM! Il sistema non si frega. + final String? email; + final String? phoneNumber; + final String? jobTitle; + final SystemRole systemRole; + final bool isActive; const StaffMemberModel({ this.id, required this.companyId, - required this.storeId, required this.userId, required this.name, - required this.surname, + this.email, + this.phoneNumber, this.jobTitle, - this.systemRole = SystemRole.user, // Sicurezza di default + this.systemRole = SystemRole.user, + this.isActive = true, }); StaffMemberModel copyWith({ String? id, String? companyId, - String? storeId, String? userId, String? name, String? surname, + String? email, + String? phoneNumber, String? jobTitle, SystemRole? systemRole, + bool? isActive, }) { return StaffMemberModel( id: id ?? this.id, companyId: companyId ?? this.companyId, - storeId: storeId ?? this.storeId, userId: userId ?? this.userId, name: name ?? this.name, - surname: surname ?? this.surname, + email: email ?? this.email, + phoneNumber: phoneNumber ?? this.phoneNumber, jobTitle: jobTitle ?? this.jobTitle, systemRole: systemRole ?? this.systemRole, + isActive: isActive ?? this.isActive, + ); + } + + factory StaffMemberModel.empty() { + return const StaffMemberModel( + companyId: '', + userId: '', + name: '', + email: '', + phoneNumber: '', + jobTitle: '', + systemRole: SystemRole.user, + isActive: true, ); } @@ -63,14 +80,13 @@ class StaffMemberModel extends Equatable { return StaffMemberModel( id: map['id'] as String?, companyId: map['company_id'] ?? '', - storeId: map['store_id'] ?? '', userId: map['user_id'] ?? '', name: map['name'] ?? '', - surname: map['surname'] ?? '', - jobTitle: map['job_title'] as String?, // Semplice stringa - systemRole: SystemRole.fromString( - map['system_role'], - ), // Lettura tipizzata + email: map['email'] as String?, + phoneNumber: map['phone_number'] as String?, + jobTitle: map['job_title'] as String?, + systemRole: SystemRole.fromString(map['system_role']), + isActive: map['is_active'] ?? true, ); } @@ -78,12 +94,13 @@ class StaffMemberModel extends Equatable { return { if (id != null) 'id': id, 'company_id': companyId, - 'store_id': storeId, 'user_id': userId, 'name': name, - 'surname': surname, + if (email != null) 'email': email, + if (phoneNumber != null) 'phone_number': phoneNumber, if (jobTitle != null) 'job_title': jobTitle, 'system_role': systemRole.name, // Trasforma SystemRole.admin in 'admin' + 'is_active': isActive, }; } @@ -91,11 +108,12 @@ class StaffMemberModel extends Equatable { List get props => [ id, companyId, - storeId, userId, name, - surname, + email, + phoneNumber, jobTitle, systemRole, + isActive, ]; } diff --git a/lib/features/master_data/staff/ui/staff_screen.dart b/lib/features/master_data/staff/ui/staff_screen.dart index b1dd3bb..bb70842 100644 --- a/lib/features/master_data/staff/ui/staff_screen.dart +++ b/lib/features/master_data/staff/ui/staff_screen.dart @@ -135,8 +135,13 @@ class _StaffScreenState extends State { subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (member.email.isNotEmpty) Text(member.email), - Text(member.phone.isNotEmpty ? member.phone : "Nessun telefono"), + if (member.email != null && member.email!.isNotEmpty) + Text(member.email!), + Text( + member.phoneNumber != null && member.phoneNumber!.isNotEmpty + ? member.phoneNumber! + : "Nessun telefono", + ), ], ), trailing: const Icon(Icons.edit_note), @@ -148,7 +153,7 @@ class _StaffScreenState extends State { void _openStaffForm(BuildContext context, {StaffMemberModel? member}) { final nameController = TextEditingController(text: member?.name); final emailController = TextEditingController(text: member?.email); - final phoneController = TextEditingController(text: member?.phone); + final phoneController = TextEditingController(text: member?.phoneNumber); // 1. Inizializziamo la lista temporanea attingendo dallo stato del Cubit // Usiamo storesByStaff (la mappa che indicizza i negozi per ogni ID dipendente) @@ -264,16 +269,16 @@ class _StaffScreenState extends State { child: ElevatedButton( onPressed: () { final companyId = context - .read() + .read() .state .company! - .id; - - final updatedMember = StaffMemberModel( + .id!; + //TODO sistemare StaffScreen per il nuovo modello + /* final updatedMember = StaffMemberModel( id: member?.id, name: nameController.text, email: emailController.text, - phone: phoneController.text, + phoneNumber: phoneController.text, companyId: companyId, ); @@ -281,7 +286,7 @@ class _StaffScreenState extends State { context.read().saveStaffWithStores( member: updatedMember, selectedStoreIds: tempSelectedStores, - ); + ); */ Navigator.pop(context); }, diff --git a/lib/features/master_data/store/bloc/store_cubit.dart b/lib/features/master_data/store/bloc/store_cubit.dart index 9d83387..09d31d0 100644 --- a/lib/features/master_data/store/bloc/store_cubit.dart +++ b/lib/features/master_data/store/bloc/store_cubit.dart @@ -13,7 +13,7 @@ part 'store_state.dart'; class StoreCubit extends Cubit { final StoreRepository _repository = GetIt.I(); final StaffRepository _staffRepository = GetIt.I(); - final SessionBloc _sessionBloc = GetIt.I(); + final SessionCubit _sessionCubit = GetIt.I(); StoreCubit() : super(const StoreState(stores: [])); @@ -33,7 +33,7 @@ class StoreCubit extends Cubit { emit(state.copyWith(status: StoreStatus.loading)); try { final stores = await _repository.fetchAllCompanyStores( - _sessionBloc.state.company!.id, + _sessionCubit.state.company!.id!, ); final Map> staffByStore = {}; for (StoreModel store in stores) { diff --git a/lib/features/master_data/store/models/store_model.dart b/lib/features/master_data/store/models/store_model.dart index 5f65f86..f30e0c1 100644 --- a/lib/features/master_data/store/models/store_model.dart +++ b/lib/features/master_data/store/models/store_model.dart @@ -81,6 +81,17 @@ class StoreModel extends Equatable { ); } + factory StoreModel.empty() { + return const StoreModel( + nome: '', + companyId: '', + indirizzo: '', + cap: '', + comune: '', + provincia: '', + ); + } + factory StoreModel.fromMap(Map map) { final providersPivotList = map['associated_providers'] as List?; List providers = []; diff --git a/lib/features/master_data/store/ui/create_store_screen.dart b/lib/features/master_data/store/ui/create_store_screen.dart index 77fd41c..7ea5786 100644 --- a/lib/features/master_data/store/ui/create_store_screen.dart +++ b/lib/features/master_data/store/ui/create_store_screen.dart @@ -34,7 +34,7 @@ class _CreateStoreScreenState extends State { /// Funzione magica per copiare i dati dall'azienda salvata in GetIt void _useCompanyAddress() { - final company = context.read().state.company; + final company = context.read().state.company; if (company != null) { setState(() { _indirizzoController.text = company.indirizzo; @@ -58,7 +58,7 @@ class _CreateStoreScreenState extends State { void _onSave() { if (_formKey.currentState!.validate()) { - final company = context.read().state.company; + final company = context.read().state.company; if (company == null) { ScaffoldMessenger.of(context).showSnackBar( @@ -69,7 +69,7 @@ class _CreateStoreScreenState extends State { final store = StoreModel( nome: _nomeController.text.trim(), - companyId: company.id, + companyId: company.id!, indirizzo: _indirizzoController.text.trim(), cap: _capController.text.trim(), comune: _comuneController.text.trim(), @@ -84,10 +84,10 @@ class _CreateStoreScreenState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Il tuo primo Negozio')), - body: BlocConsumer( - listener: (context, state) { + body: BlocBuilder( + /* listener: (context, state) { if (state.status == StoreStatus.success) { - context.read().add(AppStarted()); + context.read().; } if (state.status == StoreStatus.failure) { ScaffoldMessenger.of(context).showSnackBar( @@ -96,7 +96,7 @@ class _CreateStoreScreenState extends State { ), ); } - }, + }, */ builder: (context, state) { return SafeArea( child: SingleChildScrollView( diff --git a/lib/features/master_data/store/ui/store_form.dart b/lib/features/master_data/store/ui/store_form.dart index d9be617..1795169 100644 --- a/lib/features/master_data/store/ui/store_form.dart +++ b/lib/features/master_data/store/ui/store_form.dart @@ -130,10 +130,10 @@ class _StoreFormState extends State { comune: comuneController.text, provincia: provinciaController.text, companyId: context - .read() + .read() .state .company! - .id, // Recuperiamo la companyId + .id!, // Recuperiamo la companyId isActive: widget.store?.isActive ?? true, isPaid: widget.store?.isPaid ?? false, paymentExpiration: widget.store?.paymentExpiration, diff --git a/lib/features/onboarding/blocs/onboarding_cubit.dart b/lib/features/onboarding/blocs/onboarding_cubit.dart index 82dbea0..ef430f1 100644 --- a/lib/features/onboarding/blocs/onboarding_cubit.dart +++ b/lib/features/onboarding/blocs/onboarding_cubit.dart @@ -70,7 +70,6 @@ class OnboardingCubit extends Cubit { // PARANOIA MODE: Forziamo i legami e il ruolo di sistema 'admin' final staffToSave = staff.copyWith( companyId: state.companyId!, - storeId: state.storeId!, userId: _sessionCubit.state.user!.id, // Dall'utente loggato in Supabase systemRole: SystemRole.admin, // Blindato! ); diff --git a/lib/features/onboarding/ui/company_onboarding_form.dart b/lib/features/onboarding/ui/company_onboarding_form.dart new file mode 100644 index 0000000..1ec1484 --- /dev/null +++ b/lib/features/onboarding/ui/company_onboarding_form.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flux/core/utils/validators.dart'; +import 'package:flux/core/widgets/flux_text_field.dart'; +import 'package:flux/features/company/models/company_model.dart'; +import 'package:flux/features/onboarding/blocs/onboarding_cubit.dart'; +import 'package:flux/features/onboarding/blocs/onboarding_state.dart'; + +class CompanyOnboardingForm extends StatefulWidget { + final OnboardingState state; + const CompanyOnboardingForm({super.key, required this.state}); + + @override + State createState() => _CompanyOnboardingFormState(); +} + +class _CompanyOnboardingFormState extends State { + final _formKey = GlobalKey(); + final _nameCtrl = TextEditingController(); + + @override + void dispose() { + _nameCtrl.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(32), + child: Form( + key: _formKey, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Text( + "Iniziamo! 🏢", + style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + const Text( + "Inserisci i dati della tua attività per configurare il tuo ambiente FLUX.", + style: TextStyle(fontSize: 16, color: Colors.grey), + ), + const SizedBox(height: 48), + + FluxTextField( + label: 'Ragione Sociale / Nome Azienda', + controller: _nameCtrl, + validator: notEmptyValidator, + ), + const SizedBox(height: 16), + + const Spacer(), + ElevatedButton( + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + onPressed: () { + if (_formKey.currentState!.validate()) { + final newCompany = CompanyModel.empty().copyWith( + ragioneSociale: _nameCtrl.text.trim(), + ); + context.read().saveCompany(newCompany); + } + }, + child: const Text( + "Salva e Prosegui", + style: TextStyle(fontSize: 16), + ), + ), + const SizedBox(height: 16), + ], + ), + ), + ); + } +} diff --git a/lib/features/onboarding/ui/onboarding_screen.dart b/lib/features/onboarding/ui/onboarding_screen.dart index bc26faa..76822a2 100644 --- a/lib/features/onboarding/ui/onboarding_screen.dart +++ b/lib/features/onboarding/ui/onboarding_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; +import 'package:flux/core/utils/validators.dart'; import 'package:flux/features/company/models/company_model.dart'; import 'package:flux/features/master_data/store/models/store_model.dart'; import 'package:flux/features/master_data/staff/models/staff_member_model.dart'; @@ -9,6 +10,7 @@ import 'package:flux/features/onboarding/blocs/onboarding_cubit.dart'; // Sostituisci con il percorso corretto della tua FluxTextField import 'package:flux/core/widgets/flux_text_field.dart'; import 'package:flux/features/onboarding/blocs/onboarding_state.dart'; +import 'package:flux/features/onboarding/ui/company_onboarding_form.dart'; class OnboardingScreen extends StatefulWidget { const OnboardingScreen({super.key}); @@ -21,14 +23,10 @@ class _OnboardingScreenState extends State { late PageController _pageController; // --- CHIAVI DEI FORM (Per la validazione indipendente di ogni step) --- - final _companyFormKey = GlobalKey(); + final _storeFormKey = GlobalKey(); final _staffFormKey = GlobalKey(); - // --- CONTROLLERS: STEP 1 (Company) --- - final _companyNameCtrl = TextEditingController(); - final _companyVatCtrl = TextEditingController(); - // --- CONTROLLERS: STEP 2 (Store) --- final _storeNameCtrl = TextEditingController(); final _storeAddressCtrl = TextEditingController(); @@ -49,8 +47,6 @@ class _OnboardingScreenState extends State { @override void dispose() { _pageController.dispose(); - _companyNameCtrl.dispose(); - _companyVatCtrl.dispose(); _storeNameCtrl.dispose(); _storeAddressCtrl.dispose(); _staffFirstNameCtrl.dispose(); @@ -72,14 +68,6 @@ class _OnboardingScreenState extends State { } } - // Validatore generico riutilizzabile - String? _requireValidator(String? value) { - if (value == null || value.trim().isEmpty) { - return 'Campo obbligatorio'; - } - return null; - } - @override Widget build(BuildContext context) { return BlocConsumer( @@ -129,7 +117,7 @@ class _OnboardingScreenState extends State { physics: const NeverScrollableScrollPhysics(), // Vietato lo swipe manuale! children: [ - _buildCompanyForm(context, state), + CompanyOnboardingForm(state: state), // Step 1: Company _buildStoreForm(context, state), _buildStaffForm(context, state), ], @@ -149,73 +137,6 @@ class _OnboardingScreenState extends State { ); } - // ========================================================================= - // SCHERMATE DEI SINGOLI STEP - // ========================================================================= - - Widget _buildCompanyForm(BuildContext context, OnboardingState state) { - return Padding( - padding: const EdgeInsets.all(32.0), - child: Form( - key: _companyFormKey, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const Text( - "Iniziamo! 🏢", - style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), - const Text( - "Inserisci i dati della tua attività per configurare il tuo ambiente FLUX.", - style: TextStyle(fontSize: 16, color: Colors.grey), - ), - const SizedBox(height: 48), - - FluxTextField( - label: 'Ragione Sociale / Nome Azienda', - controller: _companyNameCtrl, - validator: _requireValidator, - ), - const SizedBox(height: 16), - FluxTextField( - label: 'Partita IVA', - controller: _companyVatCtrl, - validator: _requireValidator, - ), - - const Spacer(), - ElevatedButton( - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - onPressed: () { - if (_companyFormKey.currentState!.validate()) { - // MOCK DI ESEMPIO: Sostituisci con il tuo vero CompanyModel - final newCompany = CompanyModel( - ownerId: '', // Questo lo gestirà o ignorerà il Cubit - name: _companyNameCtrl.text.trim(), - vatNumber: _companyVatCtrl.text.trim(), - ); - context.read().saveCompany(newCompany); - } - }, - child: const Text( - "Salva e Prosegui", - style: TextStyle(fontSize: 16), - ), - ), - const SizedBox(height: 16), - ], - ), - ), - ); - } - Widget _buildStoreForm(BuildContext context, OnboardingState state) { return Padding( padding: const EdgeInsets.all(32.0), @@ -239,13 +160,13 @@ class _OnboardingScreenState extends State { FluxTextField( label: 'Nome Negozio (es. Sede Centrale)', controller: _storeNameCtrl, - validator: _requireValidator, + validator: notEmptyValidator, ), const SizedBox(height: 16), FluxTextField( label: 'Indirizzo completo', controller: _storeAddressCtrl, - validator: _requireValidator, + validator: notEmptyValidator, ), const Spacer(), @@ -258,11 +179,9 @@ class _OnboardingScreenState extends State { ), onPressed: () { if (_storeFormKey.currentState!.validate()) { - final newStore = StoreModel( - companyId: '', // Iniettato dal Cubit - name: _storeNameCtrl.text.trim(), - address: _storeAddressCtrl.text.trim(), - isActive: true, + final newStore = StoreModel.empty().copyWith( + nome: _storeNameCtrl.text.trim(), + indirizzo: _storeAddressCtrl.text.trim(), ); context.read().saveStore(newStore); } @@ -302,13 +221,13 @@ class _OnboardingScreenState extends State { FluxTextField( label: 'Nome', controller: _staffFirstNameCtrl, - validator: _requireValidator, + validator: notEmptyValidator, ), const SizedBox(height: 16), FluxTextField( label: 'Cognome', controller: _staffLastNameCtrl, - validator: _requireValidator, + validator: notEmptyValidator, ), const SizedBox(height: 16), FluxTextField( @@ -329,14 +248,9 @@ class _OnboardingScreenState extends State { ), onPressed: () { if (_staffFormKey.currentState!.validate()) { - final newStaff = StaffMemberModel( - companyId: '', // Iniettato dal Cubit - storeId: '', // Iniettato dal Cubit - userId: '', // Iniettato dal Cubit + final newStaff = StaffMemberModel.empty().copyWith( name: _staffFirstNameCtrl.text.trim(), - surname: _staffLastNameCtrl.text.trim(), jobTitle: _staffJobTitleCtrl.text.trim(), - // systemRole non viene passato qui: la Paranoia Mode del Cubit forzerà "admin" ); context.read().saveStaff(newStaff); } diff --git a/lib/features/onboarding/ui/onboarding_view.dart b/lib/features/onboarding/ui/onboarding_view.dart deleted file mode 100644 index 94223c9..0000000 --- a/lib/features/onboarding/ui/onboarding_view.dart +++ /dev/null @@ -1,211 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/core/blocs/session/session_cubit.dart'; -import 'package:flux/core/widgets/flux_text_field.dart'; -import 'package:flux/features/onboarding/blocs/onboarding_cubit.dart'; -import 'package:flux/features/onboarding/blocs/onboarding_state.dart'; -// Importa i tuoi file (cubit, modelli, ecc.) - -class OnboardingScreen extends StatefulWidget { - const OnboardingScreen({super.key}); - - @override - State createState() => _OnboardingScreenState(); -} - -class _OnboardingScreenState extends State { - late PageController _pageController; - - @override - void initState() { - super.initState(); - // Inizializziamo il controller sulla pagina giusta. - // L'indice parte da 0. company=0, store=1, staff=2. - final initialStep = context.read().state.step; - _pageController = PageController(initialPage: _getPageIndex(initialStep)); - } - - @override - void dispose() { - _pageController.dispose(); - super.dispose(); - } - - int _getPageIndex(OnboardingStep step) { - switch (step) { - case OnboardingStep.company: - return 0; - case OnboardingStep.store: - return 1; - case OnboardingStep.staff: - return 2; - default: - return 0; - } - } - - @override - Widget build(BuildContext context) { - return BlocConsumer( - // Ascoltiamo solo quando cambia lo step per animare la pagina - listenWhen: (previous, current) => previous.step != current.step, - listener: (context, state) { - if (state.step == OnboardingStep.completed) { - // Il SessionCubit prenderà il controllo e farà il redirect, - // qui potremmo mostrare un bel toast di successo. - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text("Configurazione completata! 🚀")), - ); - return; - } - - final targetPage = _getPageIndex(state.step); - _pageController.animateToPage( - targetPage, - duration: const Duration(milliseconds: 500), - curve: Curves.easeInOut, - ); - }, - builder: (context, state) { - return Scaffold( - body: SafeArea( - child: Stack( - children: [ - // Il PageView disabilita lo scorrimento manuale con NeverScrollableScrollPhysics - PageView( - controller: _pageController, - physics: const NeverScrollableScrollPhysics(), - children: [ - _buildCompanyForm(context, state), - _buildStoreForm(context, state), - _buildStaffForm( - context, - state, - ), // Qui c'è la magia paranoica - ], - ), - - // Overlay di caricamento universale - if (state.isLoading) - Container( - color: Colors.black.withValues(alpha: 0.5), - child: const Center(child: CircularProgressIndicator()), - ), - ], - ), - ), - ); - }, - ); - } - - // --- I METODI DEI FORM --- - // (Nella realtà li metterai in file separati o widget custom per pulizia) - - Widget _buildCompanyForm(BuildContext context, OnboardingState state) { - // Controller e chiavi del form... - return Padding( - padding: const EdgeInsets.all(24.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const Text( - "Step 1: La tua Azienda", - style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), - const Text("Inserisci i dati della tua attività."), - const SizedBox(height: 32), - - // Esempio usando la tua FluxTextField - FluxTextField(label: 'Ragione Sociale / Nome Azienda'), - const SizedBox(height: 16), - FluxTextField(label: 'Partita IVA'), - - const Spacer(), - ElevatedButton( - onPressed: () { - // 1. Valida il form - // 2. Crea il CompanyModel - // 3. Chiama il Cubit: - // context.read().saveCompany(newCompany); - }, - child: const Text("Salva e prosegui"), - ), - ], - ), - ); - } - - Widget _buildStoreForm(BuildContext context, OnboardingState state) { - return Padding( - padding: const EdgeInsets.all(24.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const Text( - "Step 2: Il primo Negozio", - style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), - const Text("Dove si trova il tuo punto vendita principale?"), - const SizedBox(height: 32), - - FluxTextField(label: 'Nome Negozio (es. Sede Centrale)'), - const SizedBox(height: 16), - FluxTextField(label: 'Indirizzo'), - - const Spacer(), - ElevatedButton( - onPressed: () { - // context.read().saveStore(newStore); - }, - child: const Text("Salva Negozio"), - ), - ], - ), - ); - } - - Widget _buildStaffForm(BuildContext context, OnboardingState state) { - // NOTA PARANOICA: Qui chiediamo jobTitle (Testo libero), - // ma NON diamo modo di scegliere il system_role! - return Padding( - padding: const EdgeInsets.all(24.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const Text( - "Step 3: Il tuo Profilo", - style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), - const Text("Crea il tuo profilo operativo per iniziare a lavorare."), - const SizedBox(height: 32), - - FluxTextField(label: 'Nome'), - const SizedBox(height: 16), - FluxTextField(label: 'Cognome'), - const SizedBox(height: 16), - - // TESTO LIBERO! Il cliente ci scrive quello che vuole ("CEO", "Boss", "Stagista") - FluxTextField(label: 'Ruolo / Etichetta (es. Titolare)'), - - const Spacer(), - ElevatedButton( - onPressed: () { - // Quando chiami il Cubit, passi i dati della UI. - // Ti ricordi? L'OnboardingCubit forzerà `system_role = SystemRole.admin` - // dietro le quinte! - // context.read().saveStaff(newStaff); - }, - child: const Text("Inizia a usare Flux!"), - ), - ], - ), - ); - } -} diff --git a/lib/features/services/blocs/services_cubit.dart b/lib/features/services/blocs/services_cubit.dart index ddc82e8..3d62ca4 100644 --- a/lib/features/services/blocs/services_cubit.dart +++ b/lib/features/services/blocs/services_cubit.dart @@ -16,7 +16,7 @@ part 'services_state.dart'; class ServicesCubit extends Cubit { final ServicesRepository _repository = GetIt.I(); - final SessionBloc _sessionBloc = GetIt.I(); + final SessionCubit _sessionCubit = GetIt.I(); ServicesCubit() : super(const ServicesState(status: ServicesStatus.initial)); @@ -41,7 +41,7 @@ class ServicesCubit extends Cubit { try { final currentOffset = refresh ? 0 : state.allServices.length; - final companyId = _sessionBloc.state.company?.id; + final companyId = _sessionCubit.state.company?.id; if (companyId == null) { throw Exception("Company ID non trovato nella sessione"); @@ -126,10 +126,10 @@ class ServicesCubit extends Cubit { emit( state.copyWith( currentService: ServiceModel( - storeId: _sessionBloc.state.selectedStore?.id ?? '', + storeId: _sessionCubit.state.currentStore?.id ?? '', number: '', // Sarà compilato dall'utente createdAt: DateTime.now(), - companyId: _sessionBloc.state.company!.id, + companyId: _sessionCubit.state.company!.id!, ), status: ServicesStatus.ready, ), diff --git a/lib/features/services/data/services_repository.dart b/lib/features/services/data/services_repository.dart index 52fa788..a20715d 100644 --- a/lib/features/services/data/services_repository.dart +++ b/lib/features/services/data/services_repository.dart @@ -9,7 +9,7 @@ import '../models/service_model.dart'; class ServicesRepository { final _supabase = Supabase.instance.client; - final companyId = GetIt.I.get().state.company!.id; + final companyId = GetIt.I.get().state.company!.id; final CustomerRepository _customerRepository = GetIt.I(); // --- RECUPERO SINGOLO SERVIZIO CON JOIN COMPLETO --- diff --git a/lib/features/services/ui/service_form_screen/entertainment_service_card.dart b/lib/features/services/ui/service_form_screen/entertainment_service_card.dart index e7fe8ab..4e07001 100644 --- a/lib/features/services/ui/service_form_screen/entertainment_service_card.dart +++ b/lib/features/services/ui/service_form_screen/entertainment_service_card.dart @@ -281,7 +281,7 @@ class _EntertainmentFormState extends State<_EntertainmentForm> { // Suggerimenti rapidi (Chip) FutureBuilder>( future: GetIt.I().fetchTopEntertainmentTypes( - GetIt.I().state.company!.id, + GetIt.I().state.company!.id!, ), builder: (context, snapshot) { final suggestions = snapshot.data ?? ["Netflix", "DAZN", "Sky"]; diff --git a/lib/features/services/utils/service_actions.dart b/lib/features/services/utils/service_actions.dart index 5f826b6..3159591 100644 --- a/lib/features/services/utils/service_actions.dart +++ b/lib/features/services/utils/service_actions.dart @@ -8,8 +8,8 @@ import 'package:go_router/go_router.dart'; /// Avvia la creazione di un nuovo servizio partendo dalla selezione dell'operatore. void startNewService(BuildContext context) { - final session = context.read().state; - final currentStoreId = session.selectedStore?.id; + final session = context.read().state; + final currentStoreId = session.currentStore?.id; if (currentStoreId == null) { ScaffoldMessenger.of(context).showSnackBar( @@ -59,7 +59,7 @@ void startNewService(BuildContext context) { employeeId: member.id, number: '', createdAt: DateTime.now(), - companyId: session.company!.id, + companyId: session.company!.id!, ), ); -- 2.43.0 From 497e8eb86709f3e74909cf0824fba31c0a476adb Mon Sep 17 00:00:00 2001 From: Mark M2 Macbook Date: Tue, 21 Apr 2026 11:59:57 +0200 Subject: [PATCH 3/6] s --- lib/main.dart | 1 - linux/flutter/generated_plugins.cmake | 1 + macos/Podfile.lock | 6 +++ pubspec.lock | 68 +++++++++++++++++-------- windows/flutter/generated_plugins.cmake | 1 + 5 files changed, 54 insertions(+), 23 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index e633335..5c21913 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -112,7 +112,6 @@ class _FluxAppState extends State { Widget build(BuildContext context) { return BlocBuilder( builder: (context, sessionState) { - // Usa l'enum corretto (initial, non unknown) if (sessionState.status == SessionStatus.initial) { return _buildLoadingScreen(); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 5d07423..21d8f8b 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + jni ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 4e9e7fb..a199ace 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -4,6 +4,8 @@ PODS: - file_picker (0.0.1): - FlutterMacOS - FlutterMacOS (1.0.0) + - pdfx (1.0.0): + - FlutterMacOS - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS @@ -14,6 +16,7 @@ DEPENDENCIES: - app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`) - file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`) - FlutterMacOS (from `Flutter/ephemeral`) + - pdfx (from `Flutter/ephemeral/.symlinks/plugins/pdfx/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`) @@ -24,6 +27,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos FlutterMacOS: :path: Flutter/ephemeral + pdfx: + :path: Flutter/ephemeral/.symlinks/plugins/pdfx/macos shared_preferences_foundation: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin url_launcher_macos: @@ -33,6 +38,7 @@ SPEC CHECKSUMS: app_links: 05a6ec2341985eb05e9f97dc63f5837c39895c3f file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 + pdfx: 1e79f57f7a6ce2f4a4c30f21fa54d3dc82441b51 shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd diff --git a/pubspec.lock b/pubspec.lock index 3c09862..556ab24 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -125,10 +125,10 @@ packages: dependency: transitive description: name: dart_jsonwebtoken - sha256: cb79ed79baa02b4f59a597bf365873cbd83f9bb15273d63f7803802d21717c7d + sha256: ad84e60181696513d04d5f2078e0bbc20365b911f46f647797317414bdc88fbe url: "https://pub.dev" source: hosted - version: "3.4.0" + version: "3.4.1" dbus: dependency: transitive description: @@ -210,10 +210,10 @@ packages: dependency: "direct main" description: name: flutter_dotenv - sha256: d4130c4a43e0b13fefc593bc3961f2cb46e30cb79e253d4a526b1b5d24ae1ce4 + sha256: d41da11fb497314fbf89811ec30af02d1d898b47980a129f0a8c0a1720460ba2 url: "https://pub.dev" source: hosted - version: "6.0.0" + version: "6.0.1" flutter_lints: dependency: "direct dev" description: @@ -276,10 +276,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: "48fb2f42ad057476fa4b733cb95e9f9ea7b0b010bb349ea491dca7dbdb18ffc4" + sha256: "08b742eef4f71c9df5af543751cd0b7f1c679c4088488f4223ecaddc1a813b79" url: "https://pub.dev" source: hosted - version: "17.2.0" + version: "17.2.2" google_fonts: dependency: "direct main" description: @@ -292,10 +292,10 @@ packages: dependency: transitive description: name: gotrue - sha256: ecdf3fa3ef8c5f886390ba0056d00d29138c02c39984e9caa8194dffd8a73ef7 + sha256: "7a4172601553e61716f5c3dd243aa3297e13308e07eb85b7853c941ba585dcf5" url: "https://pub.dev" source: hosted - version: "2.19.0" + version: "2.20.0" gtk: dependency: transitive description: @@ -344,6 +344,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.20.2" + jni: + dependency: transitive + description: + name: jni + sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f + url: "https://pub.dev" + source: hosted + version: "1.0.0" + jni_flutter: + dependency: transitive + description: + name: jni_flutter + sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6" + url: "https://pub.dev" + source: hosted + version: "1.0.1" jwt_decode: dependency: transitive description: @@ -448,6 +464,14 @@ packages: url: "https://pub.dev" source: hosted version: "9.3.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" path: dependency: transitive description: @@ -476,10 +500,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "149441ca6e4f38193b2e004c0ca6376a3d11f51fa5a77552d8bd4d2b0c0912ba" + sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd" url: "https://pub.dev" source: hosted - version: "2.2.23" + version: "2.3.1" path_provider_foundation: dependency: transitive description: @@ -564,10 +588,10 @@ packages: dependency: transitive description: name: postgrest - sha256: f4b6bb24b465c47649243ef0140475de8a0ec311dc9c75ebe573b2dcabb10460 + sha256: "9d61b3d4a88fcf9424d400127c54d49ed1b56ec30838fc0a33a64f31d4e694cc" url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.7.0" provider: dependency: transitive description: @@ -588,10 +612,10 @@ packages: dependency: transitive description: name: realtime_client - sha256: ee8e71af7a834e960f5b2f494f398117488036fbdb11f422611f7287fbf40562 + sha256: "7dfccf372d2f55aacfeefb6186f65a06f3ffae383fe042dbeef9d85d33487576" url: "https://pub.dev" source: hosted - version: "2.7.1" + version: "2.7.3" retry: dependency: transitive description: @@ -689,10 +713,10 @@ packages: dependency: transitive description: name: storage_client - sha256: "085a08fd67f234d575113957c04a0e8d0a3050129762f939ce831ee2c0df8257" + sha256: "4801e8ca219a35e51cbb30589aba5306667ae8935b792504595a45273cef0b18" url: "https://pub.dev" source: hosted - version: "2.5.1" + version: "2.5.2" stream_channel: dependency: transitive description: @@ -713,18 +737,18 @@ packages: dependency: transitive description: name: supabase - sha256: "89b190b585f8609fe1537cbf53eae0c9fda9b777591b064d1150c6f26e607a84" + sha256: "40e5a8833c8834e140ef53b60a6181849667eba9ca125acb7f8e24c6a769d418" url: "https://pub.dev" source: hosted - version: "2.10.4" + version: "2.10.6" supabase_flutter: dependency: "direct main" description: name: supabase_flutter - sha256: c2974cfdfeb5de517652a35f3ef0d1f3159e068de82b50ccaa27908a2b45fb82 + sha256: c02ce58abcaf86cb8055ad40bfd98bbf5b93fed3b5b56b8220d88ed03842818b url: "https://pub.dev" source: hosted - version: "2.12.2" + version: "2.12.4" synchronized: dependency: transitive description: @@ -881,10 +905,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + sha256: "046d3928e16fa4dc46e8350415661755ab759d9fc97fc21b5ab295f71e4f0499" url: "https://pub.dev" source: hosted - version: "15.0.2" + version: "15.1.0" web: dependency: transitive description: diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 24b817c..3a7bdc1 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -9,6 +9,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + jni ) set(PLUGIN_BUNDLED_LIBRARIES) -- 2.43.0 From 2b0980799f77b53bf258b5da2b9fae7d04342ba7 Mon Sep 17 00:00:00 2001 From: mark-cachy Date: Tue, 21 Apr 2026 19:16:41 +0200 Subject: [PATCH 4/6] fdds --- GEMINI.md | 7 + lib/core/blocs/session/session_cubit.dart | 1 + lib/core/blocs/session/session_events.dart | 10 - lib/core/data/core_repository.dart | 7 +- lib/core/routes/app_router.dart | 18 +- lib/core/widgets/flux_text_field.dart | 12 +- lib/features/auth/bloc/auth_cubit.dart | 9 +- .../onboarding/blocs/onboarding_cubit.dart | 11 +- .../ui/company_onboarding_form.dart | 8 +- .../onboarding/ui/onboarding_screen.dart | 74 +------ .../onboarding/ui/store_onboarding_form.dart | 189 ++++++++++++++++++ lib/main.dart | 2 + 12 files changed, 247 insertions(+), 101 deletions(-) create mode 100644 GEMINI.md delete mode 100644 lib/core/blocs/session/session_events.dart create mode 100644 lib/features/onboarding/ui/store_onboarding_form.dart diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..cd99714 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,7 @@ +## General Instructions + +- Prefer the Flutter clean architecture +- Prefer Cubit over Bloc and Stateful where is good practice +- Data Models must always extend Equatable, have copyWith, empty and fromMap, toMap when they'll be saved in db +- Use GoRouter +- Use Enums when possible instead of hardcoded Strings or Numbers for better readability and less error prone diff --git a/lib/core/blocs/session/session_cubit.dart b/lib/core/blocs/session/session_cubit.dart index d3d7d8f..6b8b592 100644 --- a/lib/core/blocs/session/session_cubit.dart +++ b/lib/core/blocs/session/session_cubit.dart @@ -20,6 +20,7 @@ class SessionCubit extends Cubit { SessionCubit(this._repository, this._prefs) : super(const SessionState(status: SessionStatus.initial)) { + initializeSession(); // Possiamo metterci in ascolto dei cambiamenti di Auth (Login/Logout) _supabase.auth.onAuthStateChange.listen((data) { final AuthChangeEvent event = data.event; diff --git a/lib/core/blocs/session/session_events.dart b/lib/core/blocs/session/session_events.dart deleted file mode 100644 index f2cd441..0000000 --- a/lib/core/blocs/session/session_events.dart +++ /dev/null @@ -1,10 +0,0 @@ -part of 'session_cubit.dart'; - -abstract class SessionEvent {} - -class AppStarted extends SessionEvent {} - -class UserChanged extends SessionEvent { - final String? userId; - UserChanged(this.userId); -} diff --git a/lib/core/data/core_repository.dart b/lib/core/data/core_repository.dart index 470f856..3eed0b6 100644 --- a/lib/core/data/core_repository.dart +++ b/lib/core/data/core_repository.dart @@ -14,10 +14,7 @@ class CoreRepository { final response = await _supabase .from('company') .select() - .eq( - 'owner_id', - userId, - ) // <-- Assicurati di avere questo campo nel DB! + .eq('user_id', userId) // <-- Assicurati di avere questo campo nel DB! .maybeSingle(); if (response == null) return null; @@ -34,7 +31,7 @@ class CoreRepository { .select() .eq('company_id', companyId) .eq('is_active', true) // Buona pratica - .order('name'); // O come si chiama il campo nome + .order('nome'); // O come si chiama il campo nome return (response as List).map((s) => StoreModel.fromMap(s)).toList(); } catch (e) { diff --git a/lib/core/routes/app_router.dart b/lib/core/routes/app_router.dart index dd59f98..624a36c 100644 --- a/lib/core/routes/app_router.dart +++ b/lib/core/routes/app_router.dart @@ -1,18 +1,22 @@ import 'dart:async'; + import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +// Importa il tuo SessionCubit e lo State +import 'package:flux/core/blocs/session/session_cubit.dart'; +import 'package:flux/core/data/core_repository.dart'; import 'package:flux/features/auth/ui/auth_screen.dart'; import 'package:flux/features/customers/models/customer_model.dart'; import 'package:flux/features/customers/ui/customer_detail_screen.dart'; import 'package:flux/features/home/ui/home_screen.dart'; import 'package:flux/features/master_data/products/ui/products_screen.dart'; +import 'package:flux/features/onboarding/blocs/onboarding_cubit.dart'; import 'package:flux/features/onboarding/ui/onboarding_screen.dart'; import 'package:flux/features/services/models/service_model.dart'; import 'package:flux/features/services/ui/service_form_screen/service_form_screen.dart'; +import 'package:get_it/get_it.dart'; import 'package:go_router/go_router.dart'; -// Importa il tuo SessionCubit e lo State -import 'package:flux/core/blocs/session/session_cubit.dart'; - class AppRouter { static GoRouter createRouter(SessionCubit sessionCubit) { return GoRouter( @@ -66,7 +70,13 @@ class AppRouter { ), GoRoute( path: '/onboarding', - builder: (context, state) => const OnboardingScreen(), + builder: (context, state) => BlocProvider( + create: (context) => OnboardingCubit( + GetIt.I.get(), + GetIt.I.get(), + ), + child: const OnboardingScreen(), + ), // Nota: All'interno di questa schermata useremo il PageView pilotato // dall'OnboardingStep. Al router non interessa quale step è attivo, // gli basta sapere che deve stare rinchiuso qui dentro! diff --git a/lib/core/widgets/flux_text_field.dart b/lib/core/widgets/flux_text_field.dart index da7055b..f8ddc5e 100644 --- a/lib/core/widgets/flux_text_field.dart +++ b/lib/core/widgets/flux_text_field.dart @@ -4,6 +4,7 @@ import 'package:flux/core/theme/theme.dart'; class FluxTextField extends StatefulWidget { final String label; + final String? labelText; final IconData? icon; final bool isPassword; final bool autoFocus; @@ -19,6 +20,7 @@ class FluxTextField extends StatefulWidget { const FluxTextField({ super.key, // Usiamo super.key per Flutter moderno required this.label, + this.labelText, this.icon, this.isPassword = false, this.autoFocus = false, @@ -60,11 +62,11 @@ class _FluxTextFieldState extends State { maxLines: widget.minLines != null ? null : widget.maxLines, style: TextStyle(color: context.primaryText), decoration: InputDecoration( - prefixIcon: Icon( - widget.icon, - color: context.accent.withValues(alpha: 0.6), - ), - labelText: widget.label, + prefixIcon: widget.icon != null + ? Icon(widget.icon, color: context.accent.withValues(alpha: 0.6)) + : null, + + labelText: widget.labelText ?? widget.label, labelStyle: TextStyle(color: context.secondaryText, fontSize: 14), filled: true, fillColor: context.surface.withValues(alpha: 0.5), diff --git a/lib/features/auth/bloc/auth_cubit.dart b/lib/features/auth/bloc/auth_cubit.dart index 734cb22..95e9b56 100644 --- a/lib/features/auth/bloc/auth_cubit.dart +++ b/lib/features/auth/bloc/auth_cubit.dart @@ -1,5 +1,6 @@ import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:get_it/get_it.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; part 'auth_state.dart'; @@ -34,14 +35,20 @@ class AuthCubit extends Cubit { password: password, ); - // Se la sessione è null, significa che Supabase ha inviato l'email di conferma if (res.session == null) { + // Caso: Conferma Email attivata su Supabase emit( state.copyWith( status: AuthStatus.initial, infoMessage: "Controlla la tua email per confermare l'account!", ), ); + } else { + // Caso: Autologin post-registrazione (Conferma email disattivata) + // 1. Fermiamo il frullino! + emit(state.copyWith(status: AuthStatus.initial)); + // 2. Svegliamo il SessionCubit! + GetIt.I().initializeSession(); } // Se non è null, ha fatto il login automatico. Stessa cosa di sopra, ci pensa il SessionCubit. } diff --git a/lib/features/onboarding/blocs/onboarding_cubit.dart b/lib/features/onboarding/blocs/onboarding_cubit.dart index ef430f1..d23f876 100644 --- a/lib/features/onboarding/blocs/onboarding_cubit.dart +++ b/lib/features/onboarding/blocs/onboarding_cubit.dart @@ -5,6 +5,8 @@ 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:flux/features/onboarding/blocs/onboarding_state.dart'; +import 'package:get_it/get_it.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; class OnboardingCubit extends Cubit { final CoreRepository _repository; @@ -14,8 +16,15 @@ class OnboardingCubit extends Cubit { : super(OnboardingState(step: _sessionCubit.state.onboardingStep)); // --- STEP 1: REGISTRAZIONE AZIENDA --- - Future saveCompany(CompanyModel company) async { + Future saveCompany(String companyName) async { emit(state.copyWith(isLoading: true)); + final company = CompanyModel.empty().copyWith( + ragioneSociale: companyName, + userId: GetIt.I().auth.currentUser!.id, + subscriptionTier: SubscriptionTier.pro, + subscriptionStatus: SubscriptionStatus.trialing, + trialEndsAt: DateTime.now().add(const Duration(days: 14)), + ); try { // Il repository restituisce il modello creato con l'ID di Supabase final savedCompany = await _repository.createCompany(company); diff --git a/lib/features/onboarding/ui/company_onboarding_form.dart b/lib/features/onboarding/ui/company_onboarding_form.dart index 1ec1484..9225da6 100644 --- a/lib/features/onboarding/ui/company_onboarding_form.dart +++ b/lib/features/onboarding/ui/company_onboarding_form.dart @@ -41,7 +41,7 @@ class _CompanyOnboardingFormState extends State { ), const SizedBox(height: 8), const Text( - "Inserisci i dati della tua attività per configurare il tuo ambiente FLUX.", + "Come si chiama la tua Azienda? \n(Potrai inserire i dati di fatturazione in seguito).", style: TextStyle(fontSize: 16, color: Colors.grey), ), const SizedBox(height: 48), @@ -51,7 +51,6 @@ class _CompanyOnboardingFormState extends State { controller: _nameCtrl, validator: notEmptyValidator, ), - const SizedBox(height: 16), const Spacer(), ElevatedButton( @@ -63,10 +62,9 @@ class _CompanyOnboardingFormState extends State { ), onPressed: () { if (_formKey.currentState!.validate()) { - final newCompany = CompanyModel.empty().copyWith( - ragioneSociale: _nameCtrl.text.trim(), + context.read().saveCompany( + _nameCtrl.text.trim(), ); - context.read().saveCompany(newCompany); } }, child: const Text( diff --git a/lib/features/onboarding/ui/onboarding_screen.dart b/lib/features/onboarding/ui/onboarding_screen.dart index 76822a2..46a8906 100644 --- a/lib/features/onboarding/ui/onboarding_screen.dart +++ b/lib/features/onboarding/ui/onboarding_screen.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; +import 'package:flux/core/data/core_repository.dart'; import 'package:flux/core/utils/validators.dart'; -import 'package:flux/features/company/models/company_model.dart'; import 'package:flux/features/master_data/store/models/store_model.dart'; import 'package:flux/features/master_data/staff/models/staff_member_model.dart'; import 'package:flux/features/onboarding/blocs/onboarding_cubit.dart'; @@ -11,6 +11,8 @@ import 'package:flux/features/onboarding/blocs/onboarding_cubit.dart'; import 'package:flux/core/widgets/flux_text_field.dart'; import 'package:flux/features/onboarding/blocs/onboarding_state.dart'; import 'package:flux/features/onboarding/ui/company_onboarding_form.dart'; +import 'package:flux/features/onboarding/ui/store_onboarding_form.dart'; +import 'package:get_it/get_it.dart'; class OnboardingScreen extends StatefulWidget { const OnboardingScreen({super.key}); @@ -24,13 +26,8 @@ class _OnboardingScreenState extends State { // --- CHIAVI DEI FORM (Per la validazione indipendente di ogni step) --- - final _storeFormKey = GlobalKey(); final _staffFormKey = GlobalKey(); - // --- CONTROLLERS: STEP 2 (Store) --- - final _storeNameCtrl = TextEditingController(); - final _storeAddressCtrl = TextEditingController(); - // --- CONTROLLERS: STEP 3 (Staff) --- final _staffFirstNameCtrl = TextEditingController(); final _staffLastNameCtrl = TextEditingController(); @@ -47,8 +44,6 @@ class _OnboardingScreenState extends State { @override void dispose() { _pageController.dispose(); - _storeNameCtrl.dispose(); - _storeAddressCtrl.dispose(); _staffFirstNameCtrl.dispose(); _staffLastNameCtrl.dispose(); _staffJobTitleCtrl.dispose(); @@ -118,7 +113,7 @@ class _OnboardingScreenState extends State { const NeverScrollableScrollPhysics(), // Vietato lo swipe manuale! children: [ CompanyOnboardingForm(state: state), // Step 1: Company - _buildStoreForm(context, state), + StoreOnboardingForm(state: state), _buildStaffForm(context, state), ], ), @@ -137,67 +132,6 @@ class _OnboardingScreenState extends State { ); } - Widget _buildStoreForm(BuildContext context, OnboardingState state) { - return Padding( - padding: const EdgeInsets.all(32.0), - child: Form( - key: _storeFormKey, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const Text( - "Il tuo Negozio 🏪", - style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), - const Text( - "Dove si trova il tuo punto vendita principale? (Potrai aggiungerne altri in seguito).", - style: TextStyle(fontSize: 16, color: Colors.grey), - ), - const SizedBox(height: 48), - - FluxTextField( - label: 'Nome Negozio (es. Sede Centrale)', - controller: _storeNameCtrl, - validator: notEmptyValidator, - ), - const SizedBox(height: 16), - FluxTextField( - label: 'Indirizzo completo', - controller: _storeAddressCtrl, - validator: notEmptyValidator, - ), - - const Spacer(), - ElevatedButton( - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - onPressed: () { - if (_storeFormKey.currentState!.validate()) { - final newStore = StoreModel.empty().copyWith( - nome: _storeNameCtrl.text.trim(), - indirizzo: _storeAddressCtrl.text.trim(), - ); - context.read().saveStore(newStore); - } - }, - child: const Text( - "Salva Negozio", - style: TextStyle(fontSize: 16), - ), - ), - const SizedBox(height: 16), - ], - ), - ), - ); - } - Widget _buildStaffForm(BuildContext context, OnboardingState state) { return Padding( padding: const EdgeInsets.all(32.0), diff --git a/lib/features/onboarding/ui/store_onboarding_form.dart b/lib/features/onboarding/ui/store_onboarding_form.dart new file mode 100644 index 0000000..d8fd4d2 --- /dev/null +++ b/lib/features/onboarding/ui/store_onboarding_form.dart @@ -0,0 +1,189 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flux/core/widgets/flux_text_field.dart'; +import 'package:flux/features/master_data/store/models/store_model.dart'; +import 'package:flux/features/onboarding/blocs/onboarding_cubit.dart'; +import 'package:flux/features/onboarding/blocs/onboarding_state.dart'; + +class StoreOnboardingForm extends StatefulWidget { + final OnboardingState state; + const StoreOnboardingForm({super.key, required this.state}); + + @override + State createState() => _StoreOnboardingFormState(); +} + +class _StoreOnboardingFormState extends State { + final _formKey = GlobalKey(); + final _nameCtrl = TextEditingController(); + final _addressCtrl = TextEditingController(); + final _cityCtrl = TextEditingController(); + final _zipCodeCtrl = TextEditingController(); + final _provinceCtrl = TextEditingController(); + + @override + void dispose() { + _nameCtrl.dispose(); + _addressCtrl.dispose(); + _cityCtrl.dispose(); + _zipCodeCtrl.dispose(); + _provinceCtrl.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(32.0), + child: Form( + key: _formKey, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Text( + "Il tuo Negozio 🏪", + style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + const Text( + "Dove si trova il tuo punto vendita principale? (Potrai aggiungerne altri in seguito).", + style: TextStyle(fontSize: 16), + ), + const SizedBox(height: 16), + FluxTextField( + controller: _nameCtrl, + label: "Nome del Negozio", + validator: (value) { + if (value == null || value.isEmpty) { + return "Il nome del negozio è obbligatorio"; + } + return null; + }, + ), + const SizedBox(height: 16), + FluxTextField( + controller: _addressCtrl, + label: "Indirizzo", + validator: (value) { + if (value == null || value.isEmpty) { + return "L'indirizzo è obbligatorio"; + } + return null; + }, + ), + const SizedBox(height: 16), + LayoutBuilder( + builder: (context, constraints) { + if (constraints.maxWidth < 600) { + return Column( + children: [ + SizedBox( + width: constraints.maxWidth, + child: FluxTextField( + label: 'Comune', + controller: _cityCtrl, + keyboardType: TextInputType.name, + ), + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + SizedBox( + width: constraints.maxWidth * 0.4, + child: FluxTextField( + label: 'CAP', + controller: _zipCodeCtrl, + maxLength: 5, + keyboardType: TextInputType.number, + ), + ), + const SizedBox(width: 16), + SizedBox( + width: constraints.maxWidth * 0.2, + child: FluxTextField( + label: 'Prov.', + controller: _provinceCtrl, + keyboardType: TextInputType.text, + onChanged: (value) => + _provinceCtrl.text = value.toUpperCase(), + maxLength: 2, + ), + ), + ], + ), + ], + ); + } else { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 160, + child: FluxTextField( + label: 'CAP', + controller: _zipCodeCtrl, + maxLength: 5, + keyboardType: TextInputType.number, + ), + ), + + SizedBox( + width: constraints.maxWidth * 0.5, + child: FluxTextField( + label: 'Comune', + controller: _cityCtrl, + keyboardType: TextInputType.name, + ), + ), + + SizedBox( + width: 120, + child: FluxTextField( + label: 'Prov.', + controller: _provinceCtrl, + keyboardType: TextInputType.text, + onChanged: (value) => + _provinceCtrl.text = value.toUpperCase(), + maxLength: 2, + ), + ), + ], + ); + } + }, + ), + const Spacer(), + ElevatedButton( + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + onPressed: () { + if (_formKey.currentState!.validate()) { + final newStore = StoreModel.empty().copyWith( + nome: _nameCtrl.text.trim(), + indirizzo: _addressCtrl.text.trim(), + comune: _cityCtrl.text.trim(), + cap: _zipCodeCtrl.text.trim(), + provincia: _provinceCtrl.text.trim(), + ); + context.read().saveStore(newStore); + } + }, + child: const Text( + "Salva Negozio", + style: TextStyle(fontSize: 16), + ), + ), + const SizedBox(height: 16), + ], + ), + ), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index 5c21913..7d00799 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flux/features/auth/bloc/auth_cubit.dart'; import 'package:get_it/get_it.dart'; import 'package:go_router/go_router.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -34,6 +35,7 @@ void main() async { runApp( MultiBlocProvider( providers: [ + BlocProvider(create: (context) => AuthCubit()), BlocProvider( create: (context) => ThemeBloc()..add(LoadThemeEvent()), ), -- 2.43.0 From 35ccd1487b47c982f56ebd7880380f1c9c4b3cb7 Mon Sep 17 00:00:00 2001 From: mark-cachy Date: Tue, 21 Apr 2026 19:25:29 +0200 Subject: [PATCH 5/6] df --- lib/core/widgets/flux_text_field.dart | 7 + .../onboarding/ui/store_onboarding_form.dart | 147 ++++++++---------- 2 files changed, 73 insertions(+), 81 deletions(-) diff --git a/lib/core/widgets/flux_text_field.dart b/lib/core/widgets/flux_text_field.dart index f8ddc5e..0c72498 100644 --- a/lib/core/widgets/flux_text_field.dart +++ b/lib/core/widgets/flux_text_field.dart @@ -1,5 +1,6 @@ // lib/ui/common/flux_text_field.dart import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flux/core/theme/theme.dart'; class FluxTextField extends StatefulWidget { @@ -16,6 +17,8 @@ class FluxTextField extends StatefulWidget { final Function(String)? onChanged; final int? maxLength; final String? Function(String?)? validator; + final List? inputFormatters; + final TextCapitalization? textCapitalization; const FluxTextField({ super.key, // Usiamo super.key per Flutter moderno @@ -32,6 +35,8 @@ class FluxTextField extends StatefulWidget { this.onChanged, this.maxLength, this.validator, + this.inputFormatters, + this.textCapitalization, }); @override @@ -104,6 +109,8 @@ class _FluxTextFieldState extends State { onFieldSubmitted: widget.onSubmitted, onChanged: widget.onChanged, maxLength: widget.maxLength, + inputFormatters: widget.inputFormatters, + textCapitalization: widget.textCapitalization ?? TextCapitalization.none, ); } } diff --git a/lib/features/onboarding/ui/store_onboarding_form.dart b/lib/features/onboarding/ui/store_onboarding_form.dart index d8fd4d2..fa8039a 100644 --- a/lib/features/onboarding/ui/store_onboarding_form.dart +++ b/lib/features/onboarding/ui/store_onboarding_form.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; // <-- IMPORTANTE per i formatter import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/widgets/flux_text_field.dart'; import 'package:flux/features/master_data/store/models/store_model.dart'; @@ -50,111 +51,62 @@ class _StoreOnboardingFormState extends State { "Dove si trova il tuo punto vendita principale? (Potrai aggiungerne altri in seguito).", style: TextStyle(fontSize: 16), ), - const SizedBox(height: 16), + const SizedBox(height: 32), + FluxTextField( controller: _nameCtrl, label: "Nome del Negozio", - validator: (value) { - if (value == null || value.isEmpty) { - return "Il nome del negozio è obbligatorio"; - } - return null; - }, + validator: (value) => + value == null || value.isEmpty ? "Obbligatorio" : null, ), const SizedBox(height: 16), + FluxTextField( controller: _addressCtrl, label: "Indirizzo", - validator: (value) { - if (value == null || value.isEmpty) { - return "L'indirizzo è obbligatorio"; - } - return null; - }, + validator: (value) => + value == null || value.isEmpty ? "Obbligatorio" : null, ), const SizedBox(height: 16), + + // IL LAYOUT RESPONSIVO PREMIUM LayoutBuilder( builder: (context, constraints) { - if (constraints.maxWidth < 600) { + final isDesktop = constraints.maxWidth >= 600; + + if (isDesktop) { + // --- DESKTOP: Tutti su una riga --- + return Row( + crossAxisAlignment: CrossAxisAlignment + .start, // Allinea in alto se ci sono errori + children: [ + Expanded(flex: 2, child: _buildZipField()), + const SizedBox(width: 16), + Expanded(flex: 5, child: _buildCityField()), + const SizedBox(width: 16), + Expanded(flex: 2, child: _buildProvField()), + ], + ); + } else { + // --- MOBILE: Comune sopra, CAP e Provincia sotto --- return Column( children: [ - SizedBox( - width: constraints.maxWidth, - child: FluxTextField( - label: 'Comune', - controller: _cityCtrl, - keyboardType: TextInputType.name, - ), - ), + _buildCityField(), const SizedBox(height: 16), Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( - width: constraints.maxWidth * 0.4, - child: FluxTextField( - label: 'CAP', - controller: _zipCodeCtrl, - maxLength: 5, - keyboardType: TextInputType.number, - ), - ), + Expanded(flex: 3, child: _buildZipField()), const SizedBox(width: 16), - SizedBox( - width: constraints.maxWidth * 0.2, - child: FluxTextField( - label: 'Prov.', - controller: _provinceCtrl, - keyboardType: TextInputType.text, - onChanged: (value) => - _provinceCtrl.text = value.toUpperCase(), - maxLength: 2, - ), - ), + Expanded(flex: 2, child: _buildProvField()), ], ), ], ); - } else { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 160, - child: FluxTextField( - label: 'CAP', - controller: _zipCodeCtrl, - maxLength: 5, - keyboardType: TextInputType.number, - ), - ), - - SizedBox( - width: constraints.maxWidth * 0.5, - child: FluxTextField( - label: 'Comune', - controller: _cityCtrl, - keyboardType: TextInputType.name, - ), - ), - - SizedBox( - width: 120, - child: FluxTextField( - label: 'Prov.', - controller: _provinceCtrl, - keyboardType: TextInputType.text, - onChanged: (value) => - _provinceCtrl.text = value.toUpperCase(), - maxLength: 2, - ), - ), - ], - ); } }, ), + const Spacer(), ElevatedButton( style: ElevatedButton.styleFrom( @@ -165,12 +117,14 @@ class _StoreOnboardingFormState extends State { ), onPressed: () { if (_formKey.currentState!.validate()) { + // MIRACOLO DELLA FACTORY EMPTY! final newStore = StoreModel.empty().copyWith( nome: _nameCtrl.text.trim(), indirizzo: _addressCtrl.text.trim(), comune: _cityCtrl.text.trim(), cap: _zipCodeCtrl.text.trim(), - provincia: _provinceCtrl.text.trim(), + // Formattiamo in maiuscolo qui, al momento del salvataggio! + provincia: _provinceCtrl.text.trim().toUpperCase(), ); context.read().saveStore(newStore); } @@ -186,4 +140,35 @@ class _StoreOnboardingFormState extends State { ), ); } + + // --- WIDGET ESTRATTI PER PULIZIA --- + + Widget _buildCityField() { + return FluxTextField( + label: 'Comune', + controller: _cityCtrl, + keyboardType: TextInputType.name, + ); + } + + Widget _buildZipField() { + return FluxTextField( + label: 'CAP', + controller: _zipCodeCtrl, + keyboardType: TextInputType.number, + // Trucchetto Premium: Limita i caratteri ma NASCONDE il contatore UI + inputFormatters: [LengthLimitingTextInputFormatter(5)], + ); + } + + Widget _buildProvField() { + return FluxTextField( + label: 'Prov.', + controller: _provinceCtrl, + keyboardType: TextInputType.text, + // Rende la tastiera del telefono automaticamente maiuscola + textCapitalization: TextCapitalization.characters, + inputFormatters: [LengthLimitingTextInputFormatter(2)], + ); + } } -- 2.43.0 From 46058d96c8c9b9ccc89b46633beed68d4bc297a0 Mon Sep 17 00:00:00 2001 From: Mark M2 Macbook Date: Wed, 22 Apr 2026 11:05:01 +0200 Subject: [PATCH 6/6] onboarding completato --- .vscode/launch.json | 31 ++++++ lib/core/blocs/session/session_cubit.dart | 4 + lib/core/data/core_repository.dart | 14 +++ lib/core/widgets/flux_text_field.dart | 6 +- lib/features/auth/bloc/auth_cubit.dart | 5 + lib/features/auth/ui/auth_screen.dart | 5 +- lib/features/home/ui/home_screen.dart | 4 +- .../onboarding/blocs/onboarding_cubit.dart | 11 +- .../ui/company_onboarding_form.dart | 18 +-- .../onboarding/ui/onboarding_screen.dart | 94 +--------------- .../onboarding/ui/staff_onboarding_form.dart | 105 ++++++++++++++++++ .../onboarding/ui/store_onboarding_form.dart | 33 +++--- lib/main.dart | 1 + 13 files changed, 211 insertions(+), 120 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 lib/features/onboarding/ui/staff_onboarding_form.dart diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..1fb6a4c --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,31 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "flux", + "request": "launch", + "type": "dart" + }, + { + "name": "s25", + "request":"launch", + "type":"dart", + "deviceId": "RFCY51YEK1N" + }, + { + "name":"mac", + "request":"launch", + "type":"dart", + "deviceId": "macos" + } + ], + "compounds": [ + { + "name": "Compound", + "configurations": ["s25","mac"] + } + ] +} \ No newline at end of file diff --git a/lib/core/blocs/session/session_cubit.dart b/lib/core/blocs/session/session_cubit.dart index 6b8b592..e3f0f70 100644 --- a/lib/core/blocs/session/session_cubit.dart +++ b/lib/core/blocs/session/session_cubit.dart @@ -49,6 +49,8 @@ class SessionCubit extends Cubit { onboardingStep: OnboardingStep.company, ), ); + } else { + emit(state.copyWith(company: company)); } // 2. Controllo Negozi @@ -62,6 +64,8 @@ class SessionCubit extends Cubit { onboardingStep: OnboardingStep.store, ), ); + } else { + emit(state.copyWith(currentStore: stores.first)); } // 3. Controllo Staff (Paziente Zero) diff --git a/lib/core/data/core_repository.dart b/lib/core/data/core_repository.dart index 3eed0b6..eebec0f 100644 --- a/lib/core/data/core_repository.dart +++ b/lib/core/data/core_repository.dart @@ -1,6 +1,9 @@ +import 'package:flutter/foundation.dart'; +import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/features/company/models/company_model.dart'; import 'package:flux/features/master_data/store/models/store_model.dart'; import 'package:flux/features/master_data/staff/models/staff_member_model.dart'; +import 'package:get_it/get_it.dart'; // Importa i tuoi modelli... import 'package:supabase_flutter/supabase_flutter.dart'; @@ -20,6 +23,7 @@ class CoreRepository { if (response == null) return null; return CompanyModel.fromMap(response); } catch (e) { + debugPrint('Errore recupero azienda: $e'); throw Exception('Errore recupero azienda: $e'); } } @@ -35,6 +39,7 @@ class CoreRepository { return (response as List).map((s) => StoreModel.fromMap(s)).toList(); } catch (e) { + debugPrint('Errore recupero negozi: $e'); throw Exception('Errore recupero negozi: $e'); } } @@ -50,6 +55,7 @@ class CoreRepository { if (response == null) return null; return StaffMemberModel.fromMap(response); } catch (e) { + debugPrint('Errore recupero profilo staff: $e'); throw Exception('Errore recupero profilo staff: $e'); } } @@ -65,6 +71,7 @@ class CoreRepository { .single(); return CompanyModel.fromMap(response); } catch (e) { + debugPrint('Creazione azienda fallita: $e'); throw Exception('Creazione azienda fallita: $e'); } } @@ -78,6 +85,7 @@ class CoreRepository { .single(); return StoreModel.fromMap(response); } catch (e) { + debugPrint('Creazione negozio fallita: $e'); throw Exception('Creazione negozio fallita: $e'); } } @@ -89,8 +97,14 @@ class CoreRepository { .insert(staff.toMap()) .select() .single(); + final StaffMemberModel staffMember = StaffMemberModel.fromMap(response); + await _supabase.from('staff_in_stores').insert({ + 'staff_member_id': staffMember.id, + 'store_id': GetIt.I.get().state.currentStore!.id, + }); return StaffMemberModel.fromMap(response); } catch (e) { + debugPrint('Creazione profilo staff fallita: $e'); throw Exception('Creazione profilo staff fallita: $e'); } } diff --git a/lib/core/widgets/flux_text_field.dart b/lib/core/widgets/flux_text_field.dart index 0c72498..a9bafae 100644 --- a/lib/core/widgets/flux_text_field.dart +++ b/lib/core/widgets/flux_text_field.dart @@ -19,6 +19,7 @@ class FluxTextField extends StatefulWidget { final String? Function(String?)? validator; final List? inputFormatters; final TextCapitalization? textCapitalization; + final bool? autocorrect; const FluxTextField({ super.key, // Usiamo super.key per Flutter moderno @@ -37,6 +38,7 @@ class FluxTextField extends StatefulWidget { this.validator, this.inputFormatters, this.textCapitalization, + this.autocorrect, }); @override @@ -58,8 +60,9 @@ class _FluxTextFieldState extends State { controller: widget.controller, validator: widget.validator, obscureText: _obscureText, + enableSuggestions: !widget.isPassword, - autocorrect: !widget.isPassword, + autocorrect: widget.isPassword ? false : widget.autocorrect ?? true, keyboardType: widget.keyboardType, autofocus: widget.autoFocus, minLines: widget.minLines, @@ -110,6 +113,7 @@ class _FluxTextFieldState extends State { onChanged: widget.onChanged, maxLength: widget.maxLength, inputFormatters: widget.inputFormatters, + textCapitalization: widget.textCapitalization ?? TextCapitalization.none, ); } diff --git a/lib/features/auth/bloc/auth_cubit.dart b/lib/features/auth/bloc/auth_cubit.dart index 95e9b56..6f062fe 100644 --- a/lib/features/auth/bloc/auth_cubit.dart +++ b/lib/features/auth/bloc/auth_cubit.dart @@ -63,4 +63,9 @@ class AuthCubit extends Cubit { ); } } + + Future requestLogout() async { + await _supabase.auth.signOut(); + emit(state.copyWith(status: AuthStatus.initial)); + } } diff --git a/lib/features/auth/ui/auth_screen.dart b/lib/features/auth/ui/auth_screen.dart index 0f3486e..00543d9 100644 --- a/lib/features/auth/ui/auth_screen.dart +++ b/lib/features/auth/ui/auth_screen.dart @@ -100,7 +100,7 @@ class _AuthScreenState extends State { label: 'Email Aziendale', icon: Icons.email_outlined, controller: _emailController, - // TODO: Aggiungi nel tuo FluxTextField la gestione del keyboardType se non c'è già! + keyboardType: TextInputType.emailAddress, ), const SizedBox(height: 20), FluxTextField( @@ -108,7 +108,8 @@ class _AuthScreenState extends State { icon: Icons.lock_outline, isPassword: true, // Magia del FluxTextField! controller: _passwordController, - // onSubmitted: (_) => _submit(), // Se lo supporti nel tuo widget custom + onSubmitted: (_) => + _submit(), // Se lo supporti nel tuo widget custom ), const SizedBox(height: 40), diff --git a/lib/features/home/ui/home_screen.dart b/lib/features/home/ui/home_screen.dart index 59dc14e..bf11ead 100644 --- a/lib/features/home/ui/home_screen.dart +++ b/lib/features/home/ui/home_screen.dart @@ -246,9 +246,7 @@ class _HomeScreenState extends State { ), onPressed: () { Navigator.pop(dialogContext); // Chiude la Dialog - /* context.read().add( - LogoutRequested(), - ); // Esegue il logout */ + context.read().requestLogout(); // Esegue il logout }, child: const Text("Esci"), ), diff --git a/lib/features/onboarding/blocs/onboarding_cubit.dart b/lib/features/onboarding/blocs/onboarding_cubit.dart index d23f876..5c7186c 100644 --- a/lib/features/onboarding/blocs/onboarding_cubit.dart +++ b/lib/features/onboarding/blocs/onboarding_cubit.dart @@ -13,7 +13,11 @@ class OnboardingCubit extends Cubit { final SessionCubit _sessionCubit; OnboardingCubit(this._sessionCubit, this._repository) - : super(OnboardingState(step: _sessionCubit.state.onboardingStep)); + : super(OnboardingState( + step: _sessionCubit.state.onboardingStep, + companyId: _sessionCubit.state.company?.id, + storeId: _sessionCubit.state.currentStore?.id, + )); // --- STEP 1: REGISTRAZIONE AZIENDA --- Future saveCompany(String companyName) async { @@ -49,12 +53,14 @@ class OnboardingCubit extends Cubit { // --- STEP 2: REGISTRAZIONE PRIMO NEGOZIO --- Future saveStore(StoreModel store) async { if (state.companyId == null) return; + if (state.companyId == '') return; emit(state.copyWith(isLoading: true)); try { // Iniettiamo forzatamente il companyId ottenuto dallo step precedente final storeToSave = store.copyWith(companyId: state.companyId); final savedStore = await _repository.createStore(storeToSave); + _sessionCubit.changeStore(savedStore); emit( state.copyWith( @@ -72,7 +78,8 @@ class OnboardingCubit extends Cubit { // --- STEP 3: REGISTRAZIONE PROFILO STAFF (PAZIENTE ZERO) --- Future saveStaff(StaffMemberModel staff) async { - if (state.companyId == null || state.storeId == null) return; + if (state.companyId == null) return; + if (state.companyId == '') return; emit(state.copyWith(isLoading: true)); try { diff --git a/lib/features/onboarding/ui/company_onboarding_form.dart b/lib/features/onboarding/ui/company_onboarding_form.dart index 9225da6..6a06b08 100644 --- a/lib/features/onboarding/ui/company_onboarding_form.dart +++ b/lib/features/onboarding/ui/company_onboarding_form.dart @@ -50,6 +50,10 @@ class _CompanyOnboardingFormState extends State { label: 'Ragione Sociale / Nome Azienda', controller: _nameCtrl, validator: notEmptyValidator, + keyboardType: TextInputType.name, + textCapitalization: TextCapitalization.words, + autocorrect: false, + onSubmitted: (_) => _submit(), ), const Spacer(), @@ -60,13 +64,7 @@ class _CompanyOnboardingFormState extends State { borderRadius: BorderRadius.circular(12), ), ), - onPressed: () { - if (_formKey.currentState!.validate()) { - context.read().saveCompany( - _nameCtrl.text.trim(), - ); - } - }, + onPressed: () => _submit(), child: const Text( "Salva e Prosegui", style: TextStyle(fontSize: 16), @@ -78,4 +76,10 @@ class _CompanyOnboardingFormState extends State { ), ); } + + void _submit() { + if (_formKey.currentState!.validate()) { + context.read().saveCompany(_nameCtrl.text.trim()); + } + } } diff --git a/lib/features/onboarding/ui/onboarding_screen.dart b/lib/features/onboarding/ui/onboarding_screen.dart index 46a8906..926c894 100644 --- a/lib/features/onboarding/ui/onboarding_screen.dart +++ b/lib/features/onboarding/ui/onboarding_screen.dart @@ -1,18 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; -import 'package:flux/core/data/core_repository.dart'; -import 'package:flux/core/utils/validators.dart'; -import 'package:flux/features/master_data/store/models/store_model.dart'; -import 'package:flux/features/master_data/staff/models/staff_member_model.dart'; import 'package:flux/features/onboarding/blocs/onboarding_cubit.dart'; - -// Sostituisci con il percorso corretto della tua FluxTextField -import 'package:flux/core/widgets/flux_text_field.dart'; import 'package:flux/features/onboarding/blocs/onboarding_state.dart'; import 'package:flux/features/onboarding/ui/company_onboarding_form.dart'; +import 'package:flux/features/onboarding/ui/staff_onboarding_form.dart'; import 'package:flux/features/onboarding/ui/store_onboarding_form.dart'; -import 'package:get_it/get_it.dart'; class OnboardingScreen extends StatefulWidget { const OnboardingScreen({super.key}); @@ -24,15 +17,6 @@ class OnboardingScreen extends StatefulWidget { class _OnboardingScreenState extends State { late PageController _pageController; - // --- CHIAVI DEI FORM (Per la validazione indipendente di ogni step) --- - - final _staffFormKey = GlobalKey(); - - // --- CONTROLLERS: STEP 3 (Staff) --- - final _staffFirstNameCtrl = TextEditingController(); - final _staffLastNameCtrl = TextEditingController(); - final _staffJobTitleCtrl = TextEditingController(); - @override void initState() { super.initState(); @@ -44,9 +28,6 @@ class _OnboardingScreenState extends State { @override void dispose() { _pageController.dispose(); - _staffFirstNameCtrl.dispose(); - _staffLastNameCtrl.dispose(); - _staffJobTitleCtrl.dispose(); super.dispose(); } @@ -112,9 +93,9 @@ class _OnboardingScreenState extends State { physics: const NeverScrollableScrollPhysics(), // Vietato lo swipe manuale! children: [ - CompanyOnboardingForm(state: state), // Step 1: Company + CompanyOnboardingForm(state: state), StoreOnboardingForm(state: state), - _buildStaffForm(context, state), + StaffOnboardingForm(), ], ), @@ -131,73 +112,4 @@ class _OnboardingScreenState extends State { }, ); } - - Widget _buildStaffForm(BuildContext context, OnboardingState state) { - return Padding( - padding: const EdgeInsets.all(32.0), - child: Form( - key: _staffFormKey, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const Text( - "Il tuo Profilo 👤", - style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), - const Text( - "Ultimo step! Crea il tuo profilo operativo per iniziare a usare FLUX.", - style: TextStyle(fontSize: 16, color: Colors.grey), - ), - const SizedBox(height: 48), - - FluxTextField( - label: 'Nome', - controller: _staffFirstNameCtrl, - validator: notEmptyValidator, - ), - const SizedBox(height: 16), - FluxTextField( - label: 'Cognome', - controller: _staffLastNameCtrl, - validator: notEmptyValidator, - ), - const SizedBox(height: 16), - FluxTextField( - label: 'Etichetta Ruolo (es. Titolare, Manager)', - controller: _staffJobTitleCtrl, - // Il jobTitle può anche essere opzionale, decidi tu! - ), - - const Spacer(), - ElevatedButton( - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 16), - backgroundColor: Colors.black, // O il tuo context.accent - foregroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - onPressed: () { - if (_staffFormKey.currentState!.validate()) { - final newStaff = StaffMemberModel.empty().copyWith( - name: _staffFirstNameCtrl.text.trim(), - jobTitle: _staffJobTitleCtrl.text.trim(), - ); - context.read().saveStaff(newStaff); - } - }, - child: const Text( - "Entra in FLUX", - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), - ), - ), - const SizedBox(height: 16), - ], - ), - ), - ); - } } diff --git a/lib/features/onboarding/ui/staff_onboarding_form.dart b/lib/features/onboarding/ui/staff_onboarding_form.dart new file mode 100644 index 0000000..a280058 --- /dev/null +++ b/lib/features/onboarding/ui/staff_onboarding_form.dart @@ -0,0 +1,105 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flux/core/utils/validators.dart'; +import 'package:flux/core/widgets/flux_text_field.dart'; +import 'package:flux/features/master_data/staff/models/staff_member_model.dart'; +import 'package:flux/features/onboarding/blocs/onboarding_cubit.dart'; + +class StaffOnboardingForm extends StatefulWidget { + const StaffOnboardingForm({super.key}); + + @override + State createState() => _StaffOnboardingFormState(); +} + +class _StaffOnboardingFormState extends State { + final _formKey = GlobalKey(); + final _nameCtrl = TextEditingController(); + final _emailCtrl = TextEditingController(); + final _jobTitleCtrl = TextEditingController(); + + @override + void dispose() { + _nameCtrl.dispose(); + _emailCtrl.dispose(); + _jobTitleCtrl.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(32), + child: Form( + key: _formKey, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Text( + "Il tuo Profilo 👤", + style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + const Text( + "Ultimo step! Crea il tuo profilo operativo per iniziare a usare FLUX.", + style: TextStyle(fontSize: 16, color: Colors.grey), + ), + const SizedBox(height: 48), + FluxTextField( + label: 'Nome', + keyboardType: TextInputType.name, + controller: _nameCtrl, + validator: notEmptyValidator, + textCapitalization: TextCapitalization.words, + autocorrect: false, + ), + const SizedBox(height: 16), + FluxTextField( + label: 'Email', + keyboardType: TextInputType.emailAddress, + controller: _emailCtrl, + textCapitalization: TextCapitalization.none, + ), + const SizedBox(height: 16), + FluxTextField( + label: 'Etichetta Ruolo (es. Titolare, Manager)', + controller: _jobTitleCtrl, + keyboardType: TextInputType.text, + textCapitalization: TextCapitalization.words, + onSubmitted: (_) => _submit(), + ), + const Spacer(), + ElevatedButton( + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + backgroundColor: Colors.black, // O il tuo context.accent + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + onPressed: () => _submit(), + child: const Text( + "Entra in FLUX", + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + ), + const SizedBox(height: 16), + ], + ), + ), + ); + } + + void _submit() { + if (_formKey.currentState!.validate()) { + final newStaff = StaffMemberModel.empty().copyWith( + name: _nameCtrl.text.trim(), + email: _emailCtrl.text.trim(), + jobTitle: _jobTitleCtrl.text.trim(), + ); + context.read().saveStaff(newStaff); + } + } +} diff --git a/lib/features/onboarding/ui/store_onboarding_form.dart b/lib/features/onboarding/ui/store_onboarding_form.dart index fa8039a..0f26732 100644 --- a/lib/features/onboarding/ui/store_onboarding_form.dart +++ b/lib/features/onboarding/ui/store_onboarding_form.dart @@ -56,6 +56,7 @@ class _StoreOnboardingFormState extends State { FluxTextField( controller: _nameCtrl, label: "Nome del Negozio", + keyboardType: TextInputType.name, validator: (value) => value == null || value.isEmpty ? "Obbligatorio" : null, ), @@ -63,6 +64,7 @@ class _StoreOnboardingFormState extends State { FluxTextField( controller: _addressCtrl, + keyboardType: TextInputType.streetAddress, label: "Indirizzo", validator: (value) => value == null || value.isEmpty ? "Obbligatorio" : null, @@ -115,20 +117,7 @@ class _StoreOnboardingFormState extends State { borderRadius: BorderRadius.circular(12), ), ), - onPressed: () { - if (_formKey.currentState!.validate()) { - // MIRACOLO DELLA FACTORY EMPTY! - final newStore = StoreModel.empty().copyWith( - nome: _nameCtrl.text.trim(), - indirizzo: _addressCtrl.text.trim(), - comune: _cityCtrl.text.trim(), - cap: _zipCodeCtrl.text.trim(), - // Formattiamo in maiuscolo qui, al momento del salvataggio! - provincia: _provinceCtrl.text.trim().toUpperCase(), - ); - context.read().saveStore(newStore); - } - }, + onPressed: () => _submit(), child: const Text( "Salva Negozio", style: TextStyle(fontSize: 16), @@ -141,6 +130,21 @@ class _StoreOnboardingFormState extends State { ); } + void _submit() { + if (_formKey.currentState!.validate()) { + // MIRACOLO DELLA FACTORY EMPTY! + final newStore = StoreModel.empty().copyWith( + nome: _nameCtrl.text.trim(), + indirizzo: _addressCtrl.text.trim(), + comune: _cityCtrl.text.trim(), + cap: _zipCodeCtrl.text.trim(), + // Formattiamo in maiuscolo qui, al momento del salvataggio! + provincia: _provinceCtrl.text.trim().toUpperCase(), + ); + context.read().saveStore(newStore); + } + } + // --- WIDGET ESTRATTI PER PULIZIA --- Widget _buildCityField() { @@ -169,6 +173,7 @@ class _StoreOnboardingFormState extends State { // Rende la tastiera del telefono automaticamente maiuscola textCapitalization: TextCapitalization.characters, inputFormatters: [LengthLimitingTextInputFormatter(2)], + onSubmitted: (_) => _submit(), ); } } diff --git a/lib/main.dart b/lib/main.dart index 7d00799..4698234 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -66,6 +66,7 @@ Future setupLocator() async { url: dotenv.env['SUPABASE_URL'] ?? '', anonKey: dotenv.env['SUPABASE_ANON_KEY'] ?? '', ); + //await Supabase.instance.client.auth.signOut(); getIt.registerSingleton(Supabase.instance.client); // Settings -- 2.43.0