aiuto
This commit is contained in:
@@ -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<SessionEvent, SessionState> {
|
||||
final SupabaseClient _supabase = GetIt.I.get<SupabaseClient>();
|
||||
final StoreRepository _storeRepository = GetIt.I.get<StoreRepository>();
|
||||
StreamSubscription<AuthState>? _authSubscription;
|
||||
|
||||
SessionBloc() : super(const SessionState(status: SessionStatus.unknown)) {
|
||||
on<AppStarted>((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<UserChanged>((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<SharedPreferences>();
|
||||
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<void> close() {
|
||||
_authSubscription?.cancel();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
128
lib/core/blocs/session/session_cubit.dart
Normal file
128
lib/core/blocs/session/session_cubit.dart
Normal file
@@ -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<SessionState> {
|
||||
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<void> 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<void> changeStore(StoreModel newStore) async {
|
||||
if (newStore.id != null) {
|
||||
await _prefs.setString(_lastStoreKey, newStore.id!);
|
||||
emit(state.copyWith(currentStore: newStore));
|
||||
}
|
||||
}
|
||||
|
||||
// --- LOGOUT ---
|
||||
Future<void> signOut() async {
|
||||
await _supabase.auth.signOut();
|
||||
// Non serve emettere stato qui, ci pensa il listener nel costruttore!
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
part of 'session_bloc.dart';
|
||||
part of 'session_cubit.dart';
|
||||
|
||||
abstract class SessionEvent {}
|
||||
|
||||
|
||||
@@ -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<StoreModel> 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<Object?> 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<StoreModel>? 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;
|
||||
}
|
||||
|
||||
100
lib/core/data/core_repository.dart
Normal file
100
lib/core/data/core_repository.dart
Normal file
@@ -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<CompanyModel?> 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<List<StoreModel>> 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<StaffMemberModel?> 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<CompanyModel> 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<StoreModel> 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<StaffMemberModel> 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<dynamic> 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<dynamic> stream) {
|
||||
notifyListeners();
|
||||
_subscription = stream.asBroadcastStream().listen((_) => notifyListeners());
|
||||
_subscription = stream.asBroadcastStream().listen(
|
||||
(dynamic _) => notifyListeners(),
|
||||
);
|
||||
}
|
||||
|
||||
late final StreamSubscription<dynamic> _subscription;
|
||||
|
||||
@@ -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<FluxTextField> {
|
||||
|
||||
@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<FluxTextField> {
|
||||
horizontal: 16,
|
||||
vertical: 16,
|
||||
),
|
||||
|
||||
suffixIcon: widget.isPassword
|
||||
? IconButton(
|
||||
icon: Icon(
|
||||
@@ -95,7 +99,7 @@ class _FluxTextFieldState extends State<FluxTextField> {
|
||||
)
|
||||
: null, // Se non è una password, niente icona
|
||||
),
|
||||
onSubmitted: widget.onSubmitted,
|
||||
onFieldSubmitted: widget.onSubmitted,
|
||||
onChanged: widget.onChanged,
|
||||
maxLength: widget.maxLength,
|
||||
);
|
||||
|
||||
@@ -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<AuthEvent, AuthState> {
|
||||
final _supabase = GetIt.instance<SupabaseClient>();
|
||||
|
||||
AuthBloc()
|
||||
: super(const AuthState(status: AuthStatus.initial, isLoginMode: true)) {
|
||||
on<ToggleAuthMode>(
|
||||
(event, emit) => emit(state.copyWith(isLoginMode: !state.isLoginMode)),
|
||||
);
|
||||
on<LoginRequested>((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<LogoutRequested>((event, emit) async {
|
||||
await _supabase.auth.signOut();
|
||||
});
|
||||
}
|
||||
}
|
||||
59
lib/features/auth/bloc/auth_cubit.dart
Normal file
59
lib/features/auth/bloc/auth_cubit.dart
Normal file
@@ -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<AuthState> {
|
||||
final _supabase = GetIt.instance<SupabaseClient>();
|
||||
|
||||
AuthCubit() : super(const AuthState());
|
||||
|
||||
void toggleMode() {
|
||||
emit(state.copyWith(isLoginMode: !state.isLoginMode));
|
||||
}
|
||||
|
||||
Future<void> 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",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
part of 'auth_bloc.dart';
|
||||
|
||||
abstract class AuthEvent extends Equatable {
|
||||
const AuthEvent();
|
||||
|
||||
@override
|
||||
List<Object?> 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<Object?> get props => [email, password];
|
||||
}
|
||||
|
||||
class LogoutRequested extends AuthEvent {} // Logout
|
||||
@@ -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<Object?> 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<Object?> get props => [status, isLoginMode, errorMessage, infoMessage];
|
||||
}
|
||||
|
||||
@@ -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<AuthScreen> {
|
||||
final _emailController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
final _isPassword = true;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@@ -24,19 +23,43 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _submit() {
|
||||
// Chiudiamo la tastiera per fare pulizia a schermo
|
||||
FocusScope.of(context).unfocus();
|
||||
|
||||
context.read<AuthCubit>().submitAuth(
|
||||
_emailController.text.trim(),
|
||||
_passwordController.text.trim(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: BlocConsumer<AuthBloc, AuthState>(
|
||||
body: BlocConsumer<AuthCubit, AuthState>(
|
||||
// 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<AuthScreen> {
|
||||
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<AuthScreen> {
|
||||
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<AuthScreen> {
|
||||
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<AuthScreen> {
|
||||
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<AuthScreen> {
|
||||
TextButton(
|
||||
onPressed: isLoading
|
||||
? null
|
||||
: () {
|
||||
context.read<AuthBloc>().add(ToggleAuthMode());
|
||||
},
|
||||
: () => context.read<AuthCubit>().toggleMode(),
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
text: state.isLoginMode
|
||||
@@ -144,13 +170,4 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _submit() {
|
||||
context.read<AuthBloc>().add(
|
||||
LoginRequested(
|
||||
email: _emailController.text.trim(),
|
||||
password: _passwordController.text.trim(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<Object?> get props => [id, userId, ragioneSociale, partitaIva, isPaid];
|
||||
List<Object?> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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<String, dynamic> 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<String, dynamic> 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<Object?> get props => [id, name, email, phone, isActive, companyId];
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
companyId,
|
||||
storeId,
|
||||
userId,
|
||||
name,
|
||||
surname,
|
||||
jobTitle,
|
||||
systemRole,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
90
lib/features/onboarding/blocs/onboarding_cubit.dart
Normal file
90
lib/features/onboarding/blocs/onboarding_cubit.dart
Normal file
@@ -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<OnboardingState> {
|
||||
final CoreRepository _repository;
|
||||
final SessionCubit _sessionCubit;
|
||||
|
||||
OnboardingCubit(this._sessionCubit, this._repository)
|
||||
: super(OnboardingState(step: _sessionCubit.state.onboardingStep));
|
||||
|
||||
// --- STEP 1: REGISTRAZIONE AZIENDA ---
|
||||
Future<void> 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<void> 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<void> 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"),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
37
lib/features/onboarding/blocs/onboarding_state.dart
Normal file
37
lib/features/onboarding/blocs/onboarding_state.dart
Normal file
@@ -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<Object?> get props => [step, isLoading, error, companyId, storeId];
|
||||
}
|
||||
355
lib/features/onboarding/ui/onboarding_screen.dart
Normal file
355
lib/features/onboarding/ui/onboarding_screen.dart
Normal file
@@ -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<OnboardingScreen> createState() => _OnboardingScreenState();
|
||||
}
|
||||
|
||||
class _OnboardingScreenState extends State<OnboardingScreen> {
|
||||
late PageController _pageController;
|
||||
|
||||
// --- CHIAVI DEI FORM (Per la validazione indipendente di ogni step) ---
|
||||
final _companyFormKey = GlobalKey<FormState>();
|
||||
final _storeFormKey = GlobalKey<FormState>();
|
||||
final _staffFormKey = GlobalKey<FormState>();
|
||||
|
||||
// --- 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<OnboardingCubit>().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<OnboardingCubit, OnboardingState>(
|
||||
// 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<OnboardingCubit>().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<OnboardingCubit>().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<OnboardingCubit>().saveStaff(newStaff);
|
||||
}
|
||||
},
|
||||
child: const Text(
|
||||
"Entra in FLUX",
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
211
lib/features/onboarding/ui/onboarding_view.dart
Normal file
211
lib/features/onboarding/ui/onboarding_view.dart
Normal file
@@ -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<OnboardingScreen> createState() => _OnboardingScreenState();
|
||||
}
|
||||
|
||||
class _OnboardingScreenState extends State<OnboardingScreen> {
|
||||
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<OnboardingCubit>().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<OnboardingCubit, OnboardingState>(
|
||||
// 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<OnboardingCubit>().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<OnboardingCubit>().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<OnboardingCubit>().saveStaff(newStaff);
|
||||
},
|
||||
child: const Text("Inizia a usare Flux!"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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<ThemeBloc>(
|
||||
create: (context) => ThemeBloc()..add(LoadThemeEvent()),
|
||||
),
|
||||
BlocProvider<SessionBloc>(create: (_) => GetIt.I<SessionBloc>()),
|
||||
BlocProvider<AuthBloc>(create: (_) => AuthBloc()),
|
||||
BlocProvider<CompanyBloc>(create: (_) => CompanyBloc()),
|
||||
BlocProvider<StoreCubit>(create: (_) => StoreCubit()..loadStores()),
|
||||
// Il Vigile Urbano viene inizializzato!
|
||||
BlocProvider<SessionCubit>(create: (_) => GetIt.I<SessionCubit>()),
|
||||
|
||||
// Cubit delle feature
|
||||
BlocProvider<StoreCubit>(create: (_) => StoreCubit()),
|
||||
BlocProvider<CustomerCubit>(create: (_) => CustomerCubit()),
|
||||
BlocProvider<ProductCubit>(create: (_) => ProductCubit()),
|
||||
BlocProvider<StaffCubit>(create: (_) => StaffCubit()..loadAllStaff()),
|
||||
BlocProvider<StaffCubit>(create: (_) => StaffCubit()),
|
||||
BlocProvider<ServicesCubit>(create: (_) => ServicesCubit()),
|
||||
BlocProvider<ProvidersCubit>(
|
||||
create: (_) => ProvidersCubit()..loadProviders(null),
|
||||
),
|
||||
BlocProvider<ProvidersCubit>(create: (_) => ProvidersCubit()),
|
||||
],
|
||||
child: const FluxApp(),
|
||||
),
|
||||
@@ -56,6 +55,7 @@ void main() async {
|
||||
|
||||
Future<void> setupLocator() async {
|
||||
final GetIt getIt = GetIt.instance;
|
||||
|
||||
getIt.registerSingleton<SharedPreferences>(
|
||||
await SharedPreferences.getInstance(),
|
||||
);
|
||||
@@ -65,15 +65,30 @@ Future<void> setupLocator() async {
|
||||
anonKey: dotenv.env['SUPABASE_ANON_KEY'] ?? '',
|
||||
);
|
||||
getIt.registerSingleton<SupabaseClient>(Supabase.instance.client);
|
||||
|
||||
// Settings
|
||||
getIt.registerLazySingleton<AppSettings>(() => AppSettings());
|
||||
getIt.registerLazySingleton<CompanyRepository>(() => CompanyRepository());
|
||||
|
||||
// Repositories
|
||||
getIt.registerLazySingleton<CoreRepository>(
|
||||
() => CoreRepository(),
|
||||
); // <-- NUOVO
|
||||
getIt.registerLazySingleton<StoreRepository>(() => StoreRepository());
|
||||
getIt.registerLazySingleton<CustomerRepository>(() => CustomerRepository());
|
||||
getIt.registerLazySingleton<ProductRepository>(() => ProductRepository());
|
||||
getIt.registerLazySingleton<StaffRepository>(() => StaffRepository());
|
||||
getIt.registerLazySingleton<ServicesRepository>(() => ServicesRepository());
|
||||
getIt.registerLazySingleton<ProviderRepository>(() => ProviderRepository());
|
||||
getIt.registerSingleton<SessionBloc>(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>(
|
||||
SessionCubit(getIt<CoreRepository>(), getIt<SharedPreferences>()),
|
||||
);
|
||||
}
|
||||
|
||||
class FluxApp extends StatefulWidget {
|
||||
@@ -89,26 +104,28 @@ class _FluxAppState extends State<FluxApp> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Lo creiamo una volta sola all'avvio dell'app
|
||||
_router = AppRouter.createRouter(context.read<SessionBloc>());
|
||||
// Creiamo il router passandogli il Cubit per i redirect
|
||||
_router = AppRouter.createRouter(context.read<SessionCubit>());
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<SessionBloc, SessionState>(
|
||||
builder: (context, state) {
|
||||
if (state.status == SessionStatus.unknown) {
|
||||
return BlocBuilder<SessionCubit, SessionState>(
|
||||
builder: (context, sessionState) {
|
||||
// Usa l'enum corretto (initial, non unknown)
|
||||
if (sessionState.status == SessionStatus.initial) {
|
||||
return _buildLoadingScreen();
|
||||
}
|
||||
|
||||
return BlocBuilder<ThemeBloc, ThemeState>(
|
||||
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<FluxApp> {
|
||||
);
|
||||
}
|
||||
|
||||
// Una semplice schermata di caricamento coerente con il brand
|
||||
Widget _buildLoadingScreen() {
|
||||
return MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
@@ -125,7 +141,6 @@ class _FluxAppState extends State<FluxApp> {
|
||||
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(),
|
||||
|
||||
Reference in New Issue
Block a user