rework-onboarding #7
31
.vscode/launch.json
vendored
Normal file
31
.vscode/launch.json
vendored
Normal file
@@ -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"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
7
GEMINI.md
Normal file
7
GEMINI.md
Normal file
@@ -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
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
133
lib/core/blocs/session/session_cubit.dart
Normal file
133
lib/core/blocs/session/session_cubit.dart
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
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)) {
|
||||||
|
initializeSession();
|
||||||
|
// 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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
emit(state.copyWith(company: 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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
emit(state.copyWith(currentStore: stores.first));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,10 +0,0 @@
|
|||||||
part of 'session_bloc.dart';
|
|
||||||
|
|
||||||
abstract class SessionEvent {}
|
|
||||||
|
|
||||||
class AppStarted extends SessionEvent {}
|
|
||||||
|
|
||||||
class UserChanged extends SessionEvent {
|
|
||||||
final String? userId;
|
|
||||||
UserChanged(this.userId);
|
|
||||||
}
|
|
||||||
@@ -1,51 +1,69 @@
|
|||||||
part of 'session_bloc.dart';
|
part of 'session_cubit.dart';
|
||||||
|
|
||||||
|
/// Definisce lo stato macroscopico della sessione
|
||||||
enum SessionStatus {
|
enum SessionStatus {
|
||||||
unknown,
|
initial,
|
||||||
unauthenticated,
|
unauthenticated,
|
||||||
authenticatedNoCompany, // Loggato ma deve creare l'azienda
|
onboardingRequired,
|
||||||
authenticatedNoStore, // Ha l'azienda ma deve creare/scegliere il primo negozio
|
authenticated,
|
||||||
ready,
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
class SessionState extends Equatable {
|
||||||
final SessionStatus status;
|
final SessionStatus status;
|
||||||
final String? userId;
|
final User? user; // Utente di Supabase Auth
|
||||||
final CompanyModel? company;
|
final CompanyModel? company;
|
||||||
final StoreModel? selectedStore;
|
final StoreModel? currentStore;
|
||||||
final List<StoreModel> availableStores; // Utile per uno switcher in futuro
|
final StaffMemberModel? currentStaff;
|
||||||
|
final OnboardingStep onboardingStep;
|
||||||
|
|
||||||
const SessionState({
|
const SessionState({
|
||||||
this.status = SessionStatus.unknown,
|
this.status = SessionStatus.initial,
|
||||||
this.userId,
|
this.user,
|
||||||
this.company,
|
this.company,
|
||||||
this.selectedStore,
|
this.currentStore,
|
||||||
this.availableStores = const [],
|
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
|
@override
|
||||||
List<Object?> get props => [
|
List<Object?> get props => [
|
||||||
status,
|
status,
|
||||||
userId,
|
user,
|
||||||
company,
|
company,
|
||||||
selectedStore,
|
currentStore,
|
||||||
availableStores,
|
currentStaff,
|
||||||
|
onboardingStep,
|
||||||
];
|
];
|
||||||
|
|
||||||
// copyWith per aggiornare solo un pezzo (es. quando cambi negozio)
|
// Helper rapidi per la UI
|
||||||
SessionState copyWith({
|
bool get isAuthenticated => status == SessionStatus.authenticated;
|
||||||
SessionStatus? status,
|
bool get needsOnboarding => status == SessionStatus.onboardingRequired;
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
111
lib/core/data/core_repository.dart
Normal file
111
lib/core/data/core_repository.dart
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
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('user_id', userId) // <-- Assicurati di avere questo campo nel DB!
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
if (response == null) return null;
|
||||||
|
return CompanyModel.fromMap(response);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Errore recupero azienda: $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('nome'); // O come si chiama il campo nome
|
||||||
|
|
||||||
|
return (response as List).map((s) => StoreModel.fromMap(s)).toList();
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Errore recupero negozi: $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) {
|
||||||
|
debugPrint('Errore recupero profilo staff: $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) {
|
||||||
|
debugPrint('Creazione azienda fallita: $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) {
|
||||||
|
debugPrint('Creazione negozio fallita: $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();
|
||||||
|
final StaffMemberModel staffMember = StaffMemberModel.fromMap(response);
|
||||||
|
await _supabase.from('staff_in_stores').insert({
|
||||||
|
'staff_member_id': staffMember.id,
|
||||||
|
'store_id': GetIt.I.get<SessionCubit>().state.currentStore!.id,
|
||||||
|
});
|
||||||
|
return StaffMemberModel.fromMap(response);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Creazione profilo staff fallita: $e');
|
||||||
|
throw Exception('Creazione profilo staff fallita: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,67 +1,89 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flux/core/blocs/session/session_bloc.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/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/models/customer_model.dart';
|
||||||
import 'package:flux/features/customers/ui/customer_detail_screen.dart';
|
import 'package:flux/features/customers/ui/customer_detail_screen.dart';
|
||||||
import 'package:flux/features/home/ui/home_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/products/ui/products_screen.dart';
|
||||||
import 'package:flux/features/master_data/store/ui/create_store_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/models/service_model.dart';
|
||||||
import 'package:flux/features/services/ui/service_form_screen/service_form_screen.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';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
class AppRouter {
|
class AppRouter {
|
||||||
// Funzione statica per creare il router
|
static GoRouter createRouter(SessionCubit sessionCubit) {
|
||||||
static GoRouter createRouter(SessionBloc sessionBloc) {
|
|
||||||
return GoRouter(
|
return GoRouter(
|
||||||
initialLocation: '/',
|
initialLocation: '/',
|
||||||
// Ascolta i cambiamenti del Bloc per scatenare il redirect
|
// MAGIA 1: Il router "ascolta" ogni singolo respiro del SessionCubit
|
||||||
refreshListenable: _GoRouterRefreshStream(sessionBloc.stream),
|
refreshListenable: GoRouterRefreshStream(sessionCubit.stream),
|
||||||
|
|
||||||
|
// MAGIA 2: Il Buttafuori Supremo
|
||||||
redirect: (context, state) {
|
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
|
// Caso 1: L'app si sta ancora avviando.
|
||||||
final bool isUnknown = sessionState.status == SessionStatus.unknown;
|
// Restituiamo null per farlo rimanere sulla SplashScreen del main.dart
|
||||||
final bool isUnauthenticated =
|
if (sessionState.status == SessionStatus.initial) {
|
||||||
sessionState.status == SessionStatus.unauthenticated;
|
return null;
|
||||||
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';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
// Caso 3: Utente loggato MA manca un pezzo dell'azienda (Flusso Canalizzatore)
|
||||||
if (isReady && location == '/login') return '/';
|
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;
|
return null;
|
||||||
},
|
},
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(path: '/', builder: (context, state) => const HomeScreen()),
|
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/login',
|
path: '/login',
|
||||||
|
//builder: (context, state) => const LoginScreen(),
|
||||||
builder: (context, state) => const AuthScreen(),
|
builder: (context, state) => const AuthScreen(),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/create-company',
|
path: '/onboarding',
|
||||||
builder: (context, state) => const CreateCompanyScreen(),
|
builder: (context, state) => BlocProvider(
|
||||||
|
create: (context) => OnboardingCubit(
|
||||||
|
GetIt.I.get<SessionCubit>(),
|
||||||
|
GetIt.I.get<CoreRepository>(),
|
||||||
|
),
|
||||||
|
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!
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/create-store',
|
path: '/',
|
||||||
builder: (context, state) => const CreateStoreScreen(),
|
builder: (context, state) => const HomeScreen(), // La tua home
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/customer/:id',
|
path: '/customer/:id',
|
||||||
@@ -96,11 +118,14 @@ class AppRouter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Classe di supporto per convertire lo Stream del Bloc in un Listenable per GoRouter
|
/// Utility fondamentale per GoRouter: trasforma lo Stream del Cubit
|
||||||
class _GoRouterRefreshStream extends ChangeNotifier {
|
/// in un Listenable che GoRouter può ascoltare per forzare i redirect.
|
||||||
_GoRouterRefreshStream(Stream<dynamic> stream) {
|
class GoRouterRefreshStream extends ChangeNotifier {
|
||||||
|
GoRouterRefreshStream(Stream<dynamic> stream) {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
_subscription = stream.asBroadcastStream().listen((_) => notifyListeners());
|
_subscription = stream.asBroadcastStream().listen(
|
||||||
|
(dynamic _) => notifyListeners(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
late final StreamSubscription<dynamic> _subscription;
|
late final StreamSubscription<dynamic> _subscription;
|
||||||
|
|||||||
6
lib/core/utils/validators.dart
Normal file
6
lib/core/utils/validators.dart
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
String? notEmptyValidator(String? value) {
|
||||||
|
if (value == null || value.trim().isEmpty) {
|
||||||
|
return 'Campo obbligatorio';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
// lib/ui/common/flux_text_field.dart
|
// lib/ui/common/flux_text_field.dart
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flux/core/theme/theme.dart';
|
import 'package:flux/core/theme/theme.dart';
|
||||||
|
|
||||||
class FluxTextField extends StatefulWidget {
|
class FluxTextField extends StatefulWidget {
|
||||||
final String label;
|
final String label;
|
||||||
final IconData icon;
|
final String? labelText;
|
||||||
|
final IconData? icon;
|
||||||
final bool isPassword;
|
final bool isPassword;
|
||||||
final bool autoFocus;
|
final bool autoFocus;
|
||||||
final TextEditingController? controller;
|
final TextEditingController? controller;
|
||||||
@@ -14,11 +16,16 @@ class FluxTextField extends StatefulWidget {
|
|||||||
final Function(String)? onSubmitted;
|
final Function(String)? onSubmitted;
|
||||||
final Function(String)? onChanged;
|
final Function(String)? onChanged;
|
||||||
final int? maxLength;
|
final int? maxLength;
|
||||||
|
final String? Function(String?)? validator;
|
||||||
|
final List<TextInputFormatter>? inputFormatters;
|
||||||
|
final TextCapitalization? textCapitalization;
|
||||||
|
final bool? autocorrect;
|
||||||
|
|
||||||
const FluxTextField({
|
const FluxTextField({
|
||||||
super.key, // Usiamo super.key per Flutter moderno
|
super.key, // Usiamo super.key per Flutter moderno
|
||||||
required this.label,
|
required this.label,
|
||||||
required this.icon,
|
this.labelText,
|
||||||
|
this.icon,
|
||||||
this.isPassword = false,
|
this.isPassword = false,
|
||||||
this.autoFocus = false,
|
this.autoFocus = false,
|
||||||
this.controller,
|
this.controller,
|
||||||
@@ -28,6 +35,10 @@ class FluxTextField extends StatefulWidget {
|
|||||||
this.onSubmitted,
|
this.onSubmitted,
|
||||||
this.onChanged,
|
this.onChanged,
|
||||||
this.maxLength,
|
this.maxLength,
|
||||||
|
this.validator,
|
||||||
|
this.inputFormatters,
|
||||||
|
this.textCapitalization,
|
||||||
|
this.autocorrect,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -45,11 +56,13 @@ class _FluxTextFieldState extends State<FluxTextField> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return TextField(
|
return TextFormField(
|
||||||
controller: widget.controller,
|
controller: widget.controller,
|
||||||
|
validator: widget.validator,
|
||||||
obscureText: _obscureText,
|
obscureText: _obscureText,
|
||||||
|
|
||||||
enableSuggestions: !widget.isPassword,
|
enableSuggestions: !widget.isPassword,
|
||||||
autocorrect: !widget.isPassword,
|
autocorrect: widget.isPassword ? false : widget.autocorrect ?? true,
|
||||||
keyboardType: widget.keyboardType,
|
keyboardType: widget.keyboardType,
|
||||||
autofocus: widget.autoFocus,
|
autofocus: widget.autoFocus,
|
||||||
minLines: widget.minLines,
|
minLines: widget.minLines,
|
||||||
@@ -57,11 +70,11 @@ class _FluxTextFieldState extends State<FluxTextField> {
|
|||||||
maxLines: widget.minLines != null ? null : widget.maxLines,
|
maxLines: widget.minLines != null ? null : widget.maxLines,
|
||||||
style: TextStyle(color: context.primaryText),
|
style: TextStyle(color: context.primaryText),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
prefixIcon: Icon(
|
prefixIcon: widget.icon != null
|
||||||
widget.icon,
|
? Icon(widget.icon, color: context.accent.withValues(alpha: 0.6))
|
||||||
color: context.accent.withValues(alpha: 0.6),
|
: null,
|
||||||
),
|
|
||||||
labelText: widget.label,
|
labelText: widget.labelText ?? widget.label,
|
||||||
labelStyle: TextStyle(color: context.secondaryText, fontSize: 14),
|
labelStyle: TextStyle(color: context.secondaryText, fontSize: 14),
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: context.surface.withValues(alpha: 0.5),
|
fillColor: context.surface.withValues(alpha: 0.5),
|
||||||
@@ -79,6 +92,7 @@ class _FluxTextFieldState extends State<FluxTextField> {
|
|||||||
horizontal: 16,
|
horizontal: 16,
|
||||||
vertical: 16,
|
vertical: 16,
|
||||||
),
|
),
|
||||||
|
|
||||||
suffixIcon: widget.isPassword
|
suffixIcon: widget.isPassword
|
||||||
? IconButton(
|
? IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
@@ -95,9 +109,12 @@ class _FluxTextFieldState extends State<FluxTextField> {
|
|||||||
)
|
)
|
||||||
: null, // Se non è una password, niente icona
|
: null, // Se non è una password, niente icona
|
||||||
),
|
),
|
||||||
onSubmitted: widget.onSubmitted,
|
onFieldSubmitted: widget.onSubmitted,
|
||||||
onChanged: widget.onChanged,
|
onChanged: widget.onChanged,
|
||||||
maxLength: widget.maxLength,
|
maxLength: widget.maxLength,
|
||||||
|
inputFormatters: widget.inputFormatters,
|
||||||
|
|
||||||
|
textCapitalization: widget.textCapitalization ?? TextCapitalization.none,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
71
lib/features/auth/bloc/auth_cubit.dart
Normal file
71
lib/features/auth/bloc/auth_cubit.dart
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
|
||||||
|
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<SessionCubit>().initializeSession();
|
||||||
|
}
|
||||||
|
// 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",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> requestLogout() async {
|
||||||
|
await _supabase.auth.signOut();
|
||||||
|
emit(state.copyWith(status: AuthStatus.initial));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
class AuthState extends Equatable {
|
||||||
|
final AuthStatus status;
|
||||||
|
final bool isLoginMode;
|
||||||
|
final String? errorMessage;
|
||||||
|
final String? infoMessage;
|
||||||
|
|
||||||
const AuthState({
|
const AuthState({
|
||||||
required this.status,
|
this.status = AuthStatus.initial,
|
||||||
this.error,
|
this.isLoginMode = true,
|
||||||
required this.isLoginMode,
|
this.errorMessage,
|
||||||
|
this.infoMessage,
|
||||||
});
|
});
|
||||||
|
|
||||||
final AuthStatus status;
|
AuthState copyWith({
|
||||||
final String? error;
|
AuthStatus? status,
|
||||||
final bool isLoginMode;
|
bool? isLoginMode,
|
||||||
|
String? errorMessage,
|
||||||
@override
|
String? infoMessage,
|
||||||
List<Object?> get props => [status, error, isLoginMode];
|
}) {
|
||||||
|
|
||||||
AuthState copyWith({AuthStatus? status, String? error, bool? isLoginMode}) {
|
|
||||||
return AuthState(
|
return AuthState(
|
||||||
status: status ?? this.status,
|
status: status ?? this.status,
|
||||||
error: error,
|
|
||||||
isLoginMode: isLoginMode ?? this.isLoginMode,
|
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/theme/theme.dart';
|
||||||
import 'package:flux/core/widgets/flux_logo.dart';
|
import 'package:flux/core/widgets/flux_logo.dart';
|
||||||
import 'package:flux/core/widgets/flux_text_field.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 {
|
class AuthScreen extends StatefulWidget {
|
||||||
const AuthScreen({super.key});
|
const AuthScreen({super.key});
|
||||||
@@ -15,7 +15,6 @@ class AuthScreen extends StatefulWidget {
|
|||||||
class _AuthScreenState extends State<AuthScreen> {
|
class _AuthScreenState extends State<AuthScreen> {
|
||||||
final _emailController = TextEditingController();
|
final _emailController = TextEditingController();
|
||||||
final _passwordController = TextEditingController();
|
final _passwordController = TextEditingController();
|
||||||
final _isPassword = true;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
@@ -24,19 +23,43 @@ class _AuthScreenState extends State<AuthScreen> {
|
|||||||
super.dispose();
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
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) {
|
listener: (context, state) {
|
||||||
if (state.status == AuthStatus.failure) {
|
// Mostriamo l'errore se c'è
|
||||||
|
if (state.errorMessage != null) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(state.error ?? 'Errore di autenticazione'),
|
content: Text(state.errorMessage!),
|
||||||
backgroundColor: Colors.redAccent,
|
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) {
|
builder: (context, state) {
|
||||||
final isLoading = state.status == AuthStatus.loading;
|
final isLoading = state.status == AuthStatus.loading;
|
||||||
@@ -49,7 +72,7 @@ class _AuthScreenState extends State<AuthScreen> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
// --- LOGO FLUX ---
|
// --- LOGO FLUX ---
|
||||||
FluxLogoAuto(height: 80),
|
const FluxLogoAuto(height: 80),
|
||||||
const SizedBox(height: 60),
|
const SizedBox(height: 60),
|
||||||
|
|
||||||
// --- TITOLO DINAMICO ---
|
// --- TITOLO DINAMICO ---
|
||||||
@@ -83,9 +106,10 @@ class _AuthScreenState extends State<AuthScreen> {
|
|||||||
FluxTextField(
|
FluxTextField(
|
||||||
label: 'Password',
|
label: 'Password',
|
||||||
icon: Icons.lock_outline,
|
icon: Icons.lock_outline,
|
||||||
isPassword: true,
|
isPassword: true, // Magia del FluxTextField!
|
||||||
controller: _passwordController,
|
controller: _passwordController,
|
||||||
onSubmitted: (_) => _submit(),
|
onSubmitted: (_) =>
|
||||||
|
_submit(), // Se lo supporti nel tuo widget custom
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 40),
|
const SizedBox(height: 40),
|
||||||
@@ -95,7 +119,7 @@ class _AuthScreenState extends State<AuthScreen> {
|
|||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: 56,
|
height: 56,
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: isLoading ? null : () => _submit(),
|
onPressed: isLoading ? null : _submit,
|
||||||
child: isLoading
|
child: isLoading
|
||||||
? const SizedBox(
|
? const SizedBox(
|
||||||
height: 24,
|
height: 24,
|
||||||
@@ -105,7 +129,12 @@ class _AuthScreenState extends State<AuthScreen> {
|
|||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: Text(state.isLoginMode ? 'ACCEDI' : 'REGISTRATI'),
|
: Text(
|
||||||
|
state.isLoginMode ? 'ACCEDI' : 'REGISTRATI',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -114,9 +143,7 @@ class _AuthScreenState extends State<AuthScreen> {
|
|||||||
TextButton(
|
TextButton(
|
||||||
onPressed: isLoading
|
onPressed: isLoading
|
||||||
? null
|
? null
|
||||||
: () {
|
: () => context.read<AuthCubit>().toggleMode(),
|
||||||
context.read<AuthBloc>().add(ToggleAuthMode());
|
|
||||||
},
|
|
||||||
child: RichText(
|
child: RichText(
|
||||||
text: TextSpan(
|
text: TextSpan(
|
||||||
text: state.isLoginMode
|
text: state.isLoginMode
|
||||||
@@ -144,13 +171,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
|
// .select().single() trasforma la risposta nell'oggetto appena inserito
|
||||||
final response = await _supabase
|
final response = await _supabase
|
||||||
.from('company')
|
.from('company')
|
||||||
.insert(company.toJson())
|
.insert(company.toMap())
|
||||||
.select()
|
.select()
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
return CompanyModel.fromJson(response);
|
return CompanyModel.fromMap(response);
|
||||||
} on PostgrestException catch (e) {
|
} on PostgrestException catch (e) {
|
||||||
throw e.message;
|
throw e.message;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -30,7 +30,7 @@ class CompanyRepository {
|
|||||||
.eq('user_id', userId as Object)
|
.eq('user_id', userId as Object)
|
||||||
.maybeSingle();
|
.maybeSingle();
|
||||||
|
|
||||||
return response != null ? CompanyModel.fromJson(response) : null;
|
return response != null ? CompanyModel.fromMap(response) : null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,50 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
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 {
|
class CompanyModel extends Equatable {
|
||||||
final String id;
|
final String? id;
|
||||||
final DateTime? createdAt;
|
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 ragioneSociale;
|
||||||
final String indirizzo;
|
final String indirizzo;
|
||||||
final String cap;
|
final String cap;
|
||||||
@@ -12,12 +53,21 @@ class CompanyModel extends Equatable {
|
|||||||
final String partitaIva;
|
final String partitaIva;
|
||||||
final String codiceFiscale;
|
final String codiceFiscale;
|
||||||
final String codiceUnivoco;
|
final String codiceUnivoco;
|
||||||
final bool isPaid;
|
|
||||||
final DateTime? paymentExpiration;
|
|
||||||
final String companyLogo;
|
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({
|
const CompanyModel({
|
||||||
this.id = '',
|
this.id,
|
||||||
this.createdAt,
|
this.createdAt,
|
||||||
required this.userId,
|
required this.userId,
|
||||||
required this.ragioneSociale,
|
required this.ragioneSociale,
|
||||||
@@ -28,48 +78,16 @@ class CompanyModel extends Equatable {
|
|||||||
required this.partitaIva,
|
required this.partitaIva,
|
||||||
required this.codiceFiscale,
|
required this.codiceFiscale,
|
||||||
required this.codiceUnivoco,
|
required this.codiceUnivoco,
|
||||||
|
this.companyLogo = '',
|
||||||
this.isPaid = false,
|
this.isPaid = false,
|
||||||
this.paymentExpiration,
|
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({
|
CompanyModel copyWith({
|
||||||
String? id,
|
String? id,
|
||||||
DateTime? createdAt,
|
DateTime? createdAt,
|
||||||
@@ -82,10 +100,16 @@ class CompanyModel extends Equatable {
|
|||||||
String? partitaIva,
|
String? partitaIva,
|
||||||
String? codiceFiscale,
|
String? codiceFiscale,
|
||||||
String? codiceUnivoco,
|
String? codiceUnivoco,
|
||||||
|
String? companyLogo,
|
||||||
bool? isPaid,
|
bool? isPaid,
|
||||||
DateTime? paymentExpiration,
|
DateTime? paymentExpiration,
|
||||||
String? companyLogo,
|
SubscriptionTier? subscriptionTier,
|
||||||
}) => CompanyModel(
|
SubscriptionStatus? subscriptionStatus,
|
||||||
|
DateTime? trialEndsAt,
|
||||||
|
String? stripeCustomerId,
|
||||||
|
String? stripeSubscriptionId,
|
||||||
|
}) {
|
||||||
|
return CompanyModel(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
createdAt: createdAt ?? this.createdAt,
|
createdAt: createdAt ?? this.createdAt,
|
||||||
userId: userId ?? this.userId,
|
userId: userId ?? this.userId,
|
||||||
@@ -97,11 +121,193 @@ class CompanyModel extends Equatable {
|
|||||||
partitaIva: partitaIva ?? this.partitaIva,
|
partitaIva: partitaIva ?? this.partitaIva,
|
||||||
codiceFiscale: codiceFiscale ?? this.codiceFiscale,
|
codiceFiscale: codiceFiscale ?? this.codiceFiscale,
|
||||||
codiceUnivoco: codiceUnivoco ?? this.codiceUnivoco,
|
codiceUnivoco: codiceUnivoco ?? this.codiceUnivoco,
|
||||||
|
companyLogo: companyLogo ?? this.companyLogo,
|
||||||
isPaid: isPaid ?? this.isPaid,
|
isPaid: isPaid ?? this.isPaid,
|
||||||
paymentExpiration: paymentExpiration ?? this.paymentExpiration,
|
paymentExpiration: paymentExpiration ?? this.paymentExpiration,
|
||||||
companyLogo: companyLogo ?? this.companyLogo,
|
subscriptionTier: subscriptionTier ?? this.subscriptionTier,
|
||||||
|
subscriptionStatus: subscriptionStatus ?? this.subscriptionStatus,
|
||||||
|
trialEndsAt: trialEndsAt ?? this.trialEndsAt,
|
||||||
|
stripeCustomerId: stripeCustomerId ?? this.stripeCustomerId,
|
||||||
|
stripeSubscriptionId: stripeSubscriptionId ?? this.stripeSubscriptionId,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory CompanyModel.empty() {
|
||||||
|
return const CompanyModel(
|
||||||
|
id: null,
|
||||||
|
createdAt: null,
|
||||||
|
userId: '',
|
||||||
|
ragioneSociale: '',
|
||||||
|
indirizzo: '',
|
||||||
|
cap: '',
|
||||||
|
citta: '',
|
||||||
|
provincia: '',
|
||||||
|
partitaIva: '',
|
||||||
|
codiceFiscale: '',
|
||||||
|
codiceUnivoco: '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
@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/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.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/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/theme/theme.dart';
|
||||||
import 'package:flux/core/widgets/flux_text_field.dart';
|
import 'package:flux/core/widgets/flux_text_field.dart';
|
||||||
import 'package:flux/features/company/models/company_model.dart';
|
import 'package:flux/features/company/models/company_model.dart';
|
||||||
@@ -46,7 +46,7 @@ class _CreateCompanyScreenState extends State<CreateCompanyScreen> {
|
|||||||
void _onSave() {
|
void _onSave() {
|
||||||
if (_formKey.currentState!.validate()) {
|
if (_formKey.currentState!.validate()) {
|
||||||
// Recuperiamo l'ID utente attuale da Supabase o dal SessionBloc
|
// Recuperiamo l'ID utente attuale da Supabase o dal SessionBloc
|
||||||
final userId = context.read<SessionBloc>().state.userId!;
|
final userId = context.read<SessionCubit>().state.user!.id;
|
||||||
|
|
||||||
final company = CompanyModel(
|
final company = CompanyModel(
|
||||||
userId: userId,
|
userId: userId,
|
||||||
@@ -77,7 +77,7 @@ class _CreateCompanyScreenState extends State<CreateCompanyScreen> {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
// Qui chiami il tuo Bloc dell'autenticazione per fare logout
|
// Qui chiami il tuo Bloc dell'autenticazione per fare logout
|
||||||
// Esempio se hai un AuthBloc o SessionBloc:
|
// Esempio se hai un AuthBloc o SessionBloc:
|
||||||
context.read<AuthBloc>().add(LogoutRequested());
|
//context.read<AuthBloc>().add(LogoutRequested());
|
||||||
|
|
||||||
// Se vuoi solo tornare brutalmente alla login per testare il logo:
|
// Se vuoi solo tornare brutalmente alla login per testare il logo:
|
||||||
// Navigator.of(context).pushReplacementNamed('/login');
|
// Navigator.of(context).pushReplacementNamed('/login');
|
||||||
@@ -92,7 +92,7 @@ class _CreateCompanyScreenState extends State<CreateCompanyScreen> {
|
|||||||
//GetIt.I.get<AppSettings>().setCurrentCompany(state.company);
|
//GetIt.I.get<AppSettings>().setCurrentCompany(state.company);
|
||||||
|
|
||||||
// 2. Notifichiamo il SessionBloc per cambiare pagina
|
// 2. Notifichiamo il SessionBloc per cambiare pagina
|
||||||
context.read<SessionBloc>().add(AppStarted());
|
//context.read<SessionCubit>().add(AppStarted());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.status == CompanyStatus.failure) {
|
if (state.status == CompanyStatus.failure) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'dart:async'; // Serve per il Timer del debounce
|
import 'dart:async'; // Serve per il Timer del debounce
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:equatable/equatable.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/data/customer_repository.dart';
|
||||||
import 'package:flux/features/customers/models/customer_model.dart';
|
import 'package:flux/features/customers/models/customer_model.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
@@ -10,7 +10,7 @@ part 'customer_state.dart';
|
|||||||
|
|
||||||
class CustomerCubit extends Cubit<CustomerState> {
|
class CustomerCubit extends Cubit<CustomerState> {
|
||||||
final CustomerRepository _repository = GetIt.I<CustomerRepository>();
|
final CustomerRepository _repository = GetIt.I<CustomerRepository>();
|
||||||
final SessionBloc _sessionBloc = GetIt.I<SessionBloc>();
|
final SessionCubit _sessionCubit = GetIt.I<SessionCubit>();
|
||||||
|
|
||||||
// Variabile per gestire il debounce della ricerca
|
// Variabile per gestire il debounce della ricerca
|
||||||
Timer? _searchDebounce;
|
Timer? _searchDebounce;
|
||||||
@@ -22,7 +22,7 @@ class CustomerCubit extends Cubit<CustomerState> {
|
|||||||
emit(state.copyWith(status: CustomerStatus.loading));
|
emit(state.copyWith(status: CustomerStatus.loading));
|
||||||
try {
|
try {
|
||||||
final customers = await _repository.getCustomers(
|
final customers = await _repository.getCustomers(
|
||||||
_sessionBloc.state.company!.id,
|
_sessionCubit.state.company!.id!,
|
||||||
);
|
);
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(status: CustomerStatus.success, customers: customers),
|
state.copyWith(status: CustomerStatus.success, customers: customers),
|
||||||
@@ -111,7 +111,7 @@ class CustomerCubit extends Cubit<CustomerState> {
|
|||||||
// Nessun "loading" state qui, per evitare sfarfallii visivi mentre si scrive
|
// Nessun "loading" state qui, per evitare sfarfallii visivi mentre si scrive
|
||||||
try {
|
try {
|
||||||
final results = await _repository.searchCustomers(
|
final results = await _repository.searchCustomers(
|
||||||
_sessionBloc.state.company!.id,
|
_sessionCubit.state.company!.id!,
|
||||||
query,
|
query,
|
||||||
);
|
);
|
||||||
emit(
|
emit(
|
||||||
@@ -137,7 +137,7 @@ class CustomerCubit extends Cubit<CustomerState> {
|
|||||||
nome: name,
|
nome: name,
|
||||||
telefono: phone ?? '',
|
telefono: phone ?? '',
|
||||||
email: email ?? '',
|
email: email ?? '',
|
||||||
companyId: _sessionBloc.state.company!.id,
|
companyId: _sessionCubit.state.company!.id!,
|
||||||
note: '',
|
note: '',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:file_picker/file_picker.dart';
|
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/core/utils/string_extensions.dart';
|
||||||
import 'package:flux/features/customers/models/customer_file_model.dart';
|
import 'package:flux/features/customers/models/customer_file_model.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
@@ -8,7 +8,7 @@ import '../models/customer_model.dart';
|
|||||||
|
|
||||||
class CustomerRepository {
|
class CustomerRepository {
|
||||||
final SupabaseClient _supabase = GetIt.I<SupabaseClient>();
|
final SupabaseClient _supabase = GetIt.I<SupabaseClient>();
|
||||||
final String companyId = GetIt.I.get<SessionBloc>().state.company!.id;
|
final String companyId = GetIt.I.get<SessionCubit>().state.company!.id!;
|
||||||
|
|
||||||
// Crea un nuovo cliente
|
// Crea un nuovo cliente
|
||||||
Future<CustomerModel> saveCustomer(CustomerModel customer) async {
|
Future<CustomerModel> saveCustomer(CustomerModel customer) async {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.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/theme/theme.dart';
|
||||||
import 'package:flux/features/customers/blocs/customer_cubit.dart';
|
import 'package:flux/features/customers/blocs/customer_cubit.dart';
|
||||||
import 'package:flux/features/customers/models/customer_model.dart';
|
import 'package:flux/features/customers/models/customer_model.dart';
|
||||||
@@ -24,14 +24,14 @@ class _CustomersContentState extends State<CustomersContent> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _loadInitialCustomers() {
|
void _loadInitialCustomers() {
|
||||||
final companyId = context.read<SessionBloc>().state.company?.id;
|
final companyId = context.read<SessionCubit>().state.company?.id;
|
||||||
if (companyId != null) {
|
if (companyId != null) {
|
||||||
context.read<CustomerCubit>().loadCustomers();
|
context.read<CustomerCubit>().loadCustomers();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onSearch(String query) {
|
void _onSearch(String query) {
|
||||||
final companyId = context.read<SessionBloc>().state.company?.id;
|
final companyId = context.read<SessionCubit>().state.company?.id;
|
||||||
if (companyId != null) {
|
if (companyId != null) {
|
||||||
context.read<CustomerCubit>().searchCustomers(query);
|
context.read<CustomerCubit>().searchCustomers(query);
|
||||||
}
|
}
|
||||||
@@ -48,7 +48,7 @@ class _CustomersContentState extends State<CustomersContent> {
|
|||||||
child: CustomerForm(
|
child: CustomerForm(
|
||||||
customer: customer,
|
customer: customer,
|
||||||
onSave: (customerFromForm) {
|
onSave: (customerFromForm) {
|
||||||
final session = context.read<SessionBloc>().state;
|
final session = context.read<SessionCubit>().state;
|
||||||
final companyId = session.company?.id;
|
final companyId = session.company?.id;
|
||||||
|
|
||||||
if (companyId == null) return;
|
if (companyId == null) return;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.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/theme/theme.dart';
|
||||||
import 'package:flux/features/home/ui/dashboard_adaptive_grid.dart';
|
import 'package:flux/features/home/ui/dashboard_adaptive_grid.dart';
|
||||||
|
|
||||||
@@ -16,9 +16,9 @@ class DashboardContent extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocBuilder<SessionBloc, SessionState>(
|
return BlocBuilder<SessionCubit, SessionState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final store = state.selectedStore;
|
final store = state.currentStore;
|
||||||
final company = state.company;
|
final company = state.company;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.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/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/master_data/master_data_hub_content.dart';
|
||||||
import 'package:flux/features/services/blocs/services_cubit.dart';
|
import 'package:flux/features/services/blocs/services_cubit.dart';
|
||||||
import 'package:flux/features/services/ui/services_screen.dart';
|
import 'package:flux/features/services/ui/services_screen.dart';
|
||||||
@@ -30,7 +30,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocBuilder<SessionBloc, SessionState>(
|
return BlocBuilder<SessionCubit, SessionState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
@@ -203,7 +203,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Text(
|
Text(
|
||||||
GetIt.I.get<SessionBloc>().state.company?.ragioneSociale ??
|
GetIt.I.get<SessionCubit>().state.company?.ragioneSociale ??
|
||||||
"Utente",
|
"Utente",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@@ -246,9 +246,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.pop(dialogContext); // Chiude la Dialog
|
Navigator.pop(dialogContext); // Chiude la Dialog
|
||||||
context.read<AuthBloc>().add(
|
context.read<AuthCubit>().requestLogout(); // Esegue il logout
|
||||||
LogoutRequested(),
|
|
||||||
); // Esegue il logout
|
|
||||||
},
|
},
|
||||||
child: const Text("Esci"),
|
child: const Text("Esci"),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.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/data/product_repository.dart';
|
||||||
import 'package:flux/features/master_data/products/models/brand_model.dart';
|
import 'package:flux/features/master_data/products/models/brand_model.dart';
|
||||||
import 'package:flux/features/master_data/products/models/model_model.dart';
|
import 'package:flux/features/master_data/products/models/model_model.dart';
|
||||||
@@ -11,7 +11,7 @@ part 'product_state.dart';
|
|||||||
|
|
||||||
class ProductCubit extends Cubit<ProductState> {
|
class ProductCubit extends Cubit<ProductState> {
|
||||||
final ProductRepository _repository = GetIt.I<ProductRepository>();
|
final ProductRepository _repository = GetIt.I<ProductRepository>();
|
||||||
final SessionBloc _sessionBloc = GetIt.I<SessionBloc>();
|
final SessionCubit _sessionCubit = GetIt.I<SessionCubit>();
|
||||||
|
|
||||||
ProductCubit() : super(const ProductState());
|
ProductCubit() : super(const ProductState());
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ class ProductCubit extends Cubit<ProductState> {
|
|||||||
emit(state.copyWith(status: ProductStatus.loading));
|
emit(state.copyWith(status: ProductStatus.loading));
|
||||||
try {
|
try {
|
||||||
final brands = await _repository.getBrands(
|
final brands = await _repository.getBrands(
|
||||||
_sessionBloc.state.company!.id,
|
_sessionCubit.state.company!.id!,
|
||||||
);
|
);
|
||||||
emit(state.copyWith(status: ProductStatus.success, brands: brands));
|
emit(state.copyWith(status: ProductStatus.success, brands: brands));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -54,7 +54,7 @@ class ProductCubit extends Cubit<ProductState> {
|
|||||||
final brand = BrandModel(
|
final brand = BrandModel(
|
||||||
id: id,
|
id: id,
|
||||||
name: name,
|
name: name,
|
||||||
companyId: _sessionBloc.state.company!.id,
|
companyId: _sessionCubit.state.company!.id!,
|
||||||
);
|
);
|
||||||
final newBrand = await _repository.upsertBrand(brand);
|
final newBrand = await _repository.upsertBrand(brand);
|
||||||
await loadBrands(); // Ricarichiamo la lista aggiornata
|
await loadBrands(); // Ricarichiamo la lista aggiornata
|
||||||
@@ -137,7 +137,10 @@ class ProductCubit extends Cubit<ProductState> {
|
|||||||
// 1. Cerchiamo o creiamo il Brand
|
// 1. Cerchiamo o creiamo il Brand
|
||||||
// (Usa una funzione upsert o una ricerca rapida nel repository)
|
// (Usa una funzione upsert o una ricerca rapida nel repository)
|
||||||
brand ??= await _repository.upsertBrand(
|
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
|
// 2. Creiamo il Modello legato al Brand
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.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/providers/data/provider_repository.dart';
|
||||||
import 'package:flux/features/master_data/store/models/store_model.dart';
|
import 'package:flux/features/master_data/store/models/store_model.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
@@ -52,7 +52,7 @@ class ProvidersState extends Equatable {
|
|||||||
|
|
||||||
class ProvidersCubit extends Cubit<ProvidersState> {
|
class ProvidersCubit extends Cubit<ProvidersState> {
|
||||||
final ProviderRepository _repository = GetIt.I<ProviderRepository>();
|
final ProviderRepository _repository = GetIt.I<ProviderRepository>();
|
||||||
final SessionBloc _sessionBloc = GetIt.I<SessionBloc>();
|
final SessionCubit _sessionCubit = GetIt.I<SessionCubit>();
|
||||||
|
|
||||||
ProvidersCubit() : super(const ProvidersState());
|
ProvidersCubit() : super(const ProvidersState());
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ class ProvidersCubit extends Cubit<ProvidersState> {
|
|||||||
emit(state.copyWith(isLoading: true));
|
emit(state.copyWith(isLoading: true));
|
||||||
try {
|
try {
|
||||||
final all = await _repository.fetchAllCompanyProviders(
|
final all = await _repository.fetchAllCompanyProviders(
|
||||||
_sessionBloc.state.company!.id,
|
_sessionCubit.state.company!.id!,
|
||||||
);
|
);
|
||||||
List<String> associated = [];
|
List<String> associated = [];
|
||||||
|
|
||||||
@@ -135,7 +135,7 @@ class ProvidersCubit extends Cubit<ProvidersState> {
|
|||||||
) async {
|
) async {
|
||||||
emit(state.copyWith(isLoading: true));
|
emit(state.copyWith(isLoading: true));
|
||||||
// Assicuriamoci di settare la companyId prima di salvare
|
// 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 {
|
try {
|
||||||
// 1. Salviamo l'anagrafica (upsert)
|
// 1. Salviamo l'anagrafica (upsert)
|
||||||
// Se è un nuovo provider, l'ID potrebbe essere generato qui dal DB
|
// Se è un nuovo provider, l'ID potrebbe essere generato qui dal DB
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.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/data/staff_repository.dart';
|
||||||
import 'package:flux/features/master_data/staff/models/staff_member_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/master_data/store/models/store_model.dart';
|
||||||
@@ -10,7 +10,7 @@ part 'staff_state.dart';
|
|||||||
|
|
||||||
class StaffCubit extends Cubit<StaffState> {
|
class StaffCubit extends Cubit<StaffState> {
|
||||||
final StaffRepository _repository = GetIt.I.get<StaffRepository>();
|
final StaffRepository _repository = GetIt.I.get<StaffRepository>();
|
||||||
final SessionBloc _sessionBloc = GetIt.I<SessionBloc>();
|
final SessionCubit _sessionCubit = GetIt.I<SessionCubit>();
|
||||||
|
|
||||||
StaffCubit() : super(const StaffState());
|
StaffCubit() : super(const StaffState());
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ class StaffCubit extends Cubit<StaffState> {
|
|||||||
emit(state.copyWith(isLoading: true, error: null));
|
emit(state.copyWith(isLoading: true, error: null));
|
||||||
try {
|
try {
|
||||||
final staff = await _repository.getStaffMembers(
|
final staff = await _repository.getStaffMembers(
|
||||||
_sessionBloc.state.company!.id,
|
_sessionCubit.state.company!.id!,
|
||||||
);
|
);
|
||||||
final Map<String, List<StoreModel>> storesByStaff = {};
|
final Map<String, List<StoreModel>> storesByStaff = {};
|
||||||
for (StaffMemberModel member in staff) {
|
for (StaffMemberModel member in staff) {
|
||||||
|
|||||||
@@ -1,47 +1,119 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
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 {
|
class StaffMemberModel extends Equatable {
|
||||||
final String? id;
|
final String? id;
|
||||||
final String name;
|
|
||||||
final String email;
|
|
||||||
final String phone;
|
|
||||||
final bool isActive;
|
|
||||||
final String companyId;
|
final String companyId;
|
||||||
|
final String userId;
|
||||||
|
final String name;
|
||||||
|
final String? email;
|
||||||
|
final String? phoneNumber;
|
||||||
|
final String? jobTitle;
|
||||||
|
final SystemRole systemRole;
|
||||||
|
final bool isActive;
|
||||||
|
|
||||||
const StaffMemberModel({
|
const StaffMemberModel({
|
||||||
this.id,
|
this.id,
|
||||||
required this.name,
|
|
||||||
this.email = '',
|
|
||||||
this.phone = '',
|
|
||||||
this.isActive = true,
|
|
||||||
required this.companyId,
|
required this.companyId,
|
||||||
|
required this.userId,
|
||||||
|
required this.name,
|
||||||
|
this.email,
|
||||||
|
this.phoneNumber,
|
||||||
|
this.jobTitle,
|
||||||
|
this.systemRole = SystemRole.user,
|
||||||
|
this.isActive = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
StaffMemberModel copyWith({
|
||||||
|
String? id,
|
||||||
|
String? companyId,
|
||||||
|
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,
|
||||||
|
userId: userId ?? this.userId,
|
||||||
|
name: name ?? this.name,
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
factory StaffMemberModel.fromMap(Map<String, dynamic> map) {
|
factory StaffMemberModel.fromMap(Map<String, dynamic> map) {
|
||||||
return StaffMemberModel(
|
return StaffMemberModel(
|
||||||
id: map['id'],
|
id: map['id'] as String?,
|
||||||
// Applichiamo il tuo myFormat per visualizzare i nomi correttamente
|
companyId: map['company_id'] ?? '',
|
||||||
name: (map['name'] as String).myFormat(),
|
userId: map['user_id'] ?? '',
|
||||||
// L'email la teniamo lowercase per standard tecnico
|
name: map['name'] ?? '',
|
||||||
email: (map['email'] as String? ?? '').toLowerCase().trim(),
|
email: map['email'] as String?,
|
||||||
phone: (map['phone'] as String? ?? '').trim(),
|
phoneNumber: map['phone_number'] as String?,
|
||||||
|
jobTitle: map['job_title'] as String?,
|
||||||
|
systemRole: SystemRole.fromString(map['system_role']),
|
||||||
isActive: map['is_active'] ?? true,
|
isActive: map['is_active'] ?? true,
|
||||||
companyId: map['company_id'],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
Map<String, dynamic> toMap() {
|
||||||
return {
|
return {
|
||||||
if (id != null) 'id': id,
|
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,
|
'company_id': companyId,
|
||||||
|
'user_id': userId,
|
||||||
|
'name': name,
|
||||||
|
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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [id, name, email, phone, isActive, companyId];
|
List<Object?> get props => [
|
||||||
|
id,
|
||||||
|
companyId,
|
||||||
|
userId,
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
phoneNumber,
|
||||||
|
jobTitle,
|
||||||
|
systemRole,
|
||||||
|
isActive,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.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/theme/theme.dart';
|
||||||
import 'package:flux/core/widgets/flux_text_field.dart';
|
import 'package:flux/core/widgets/flux_text_field.dart';
|
||||||
import 'package:flux/features/master_data/staff/blocs/staff_cubit.dart'; // Tuo percorso
|
import 'package:flux/features/master_data/staff/blocs/staff_cubit.dart'; // Tuo percorso
|
||||||
@@ -135,8 +135,13 @@ class _StaffScreenState extends State<StaffScreen> {
|
|||||||
subtitle: Column(
|
subtitle: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (member.email.isNotEmpty) Text(member.email),
|
if (member.email != null && member.email!.isNotEmpty)
|
||||||
Text(member.phone.isNotEmpty ? member.phone : "Nessun telefono"),
|
Text(member.email!),
|
||||||
|
Text(
|
||||||
|
member.phoneNumber != null && member.phoneNumber!.isNotEmpty
|
||||||
|
? member.phoneNumber!
|
||||||
|
: "Nessun telefono",
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
trailing: const Icon(Icons.edit_note),
|
trailing: const Icon(Icons.edit_note),
|
||||||
@@ -148,7 +153,7 @@ class _StaffScreenState extends State<StaffScreen> {
|
|||||||
void _openStaffForm(BuildContext context, {StaffMemberModel? member}) {
|
void _openStaffForm(BuildContext context, {StaffMemberModel? member}) {
|
||||||
final nameController = TextEditingController(text: member?.name);
|
final nameController = TextEditingController(text: member?.name);
|
||||||
final emailController = TextEditingController(text: member?.email);
|
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
|
// 1. Inizializziamo la lista temporanea attingendo dallo stato del Cubit
|
||||||
// Usiamo storesByStaff (la mappa che indicizza i negozi per ogni ID dipendente)
|
// Usiamo storesByStaff (la mappa che indicizza i negozi per ogni ID dipendente)
|
||||||
@@ -264,16 +269,16 @@ class _StaffScreenState extends State<StaffScreen> {
|
|||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final companyId = context
|
final companyId = context
|
||||||
.read<SessionBloc>()
|
.read<SessionCubit>()
|
||||||
.state
|
.state
|
||||||
.company!
|
.company!
|
||||||
.id;
|
.id!;
|
||||||
|
//TODO sistemare StaffScreen per il nuovo modello
|
||||||
final updatedMember = StaffMemberModel(
|
/* final updatedMember = StaffMemberModel(
|
||||||
id: member?.id,
|
id: member?.id,
|
||||||
name: nameController.text,
|
name: nameController.text,
|
||||||
email: emailController.text,
|
email: emailController.text,
|
||||||
phone: phoneController.text,
|
phoneNumber: phoneController.text,
|
||||||
companyId: companyId,
|
companyId: companyId,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -281,7 +286,7 @@ class _StaffScreenState extends State<StaffScreen> {
|
|||||||
context.read<StaffCubit>().saveStaffWithStores(
|
context.read<StaffCubit>().saveStaffWithStores(
|
||||||
member: updatedMember,
|
member: updatedMember,
|
||||||
selectedStoreIds: tempSelectedStores,
|
selectedStoreIds: tempSelectedStores,
|
||||||
);
|
); */
|
||||||
|
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:equatable/equatable.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/providers/models/provider_model.dart';
|
||||||
import 'package:flux/features/master_data/staff/data/staff_repository.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/staff/models/staff_member_model.dart';
|
||||||
@@ -13,7 +13,7 @@ part 'store_state.dart';
|
|||||||
class StoreCubit extends Cubit<StoreState> {
|
class StoreCubit extends Cubit<StoreState> {
|
||||||
final StoreRepository _repository = GetIt.I<StoreRepository>();
|
final StoreRepository _repository = GetIt.I<StoreRepository>();
|
||||||
final StaffRepository _staffRepository = GetIt.I<StaffRepository>();
|
final StaffRepository _staffRepository = GetIt.I<StaffRepository>();
|
||||||
final SessionBloc _sessionBloc = GetIt.I<SessionBloc>();
|
final SessionCubit _sessionCubit = GetIt.I<SessionCubit>();
|
||||||
|
|
||||||
StoreCubit() : super(const StoreState(stores: []));
|
StoreCubit() : super(const StoreState(stores: []));
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ class StoreCubit extends Cubit<StoreState> {
|
|||||||
emit(state.copyWith(status: StoreStatus.loading));
|
emit(state.copyWith(status: StoreStatus.loading));
|
||||||
try {
|
try {
|
||||||
final stores = await _repository.fetchAllCompanyStores(
|
final stores = await _repository.fetchAllCompanyStores(
|
||||||
_sessionBloc.state.company!.id,
|
_sessionCubit.state.company!.id!,
|
||||||
);
|
);
|
||||||
final Map<String, List<StaffMemberModel>> staffByStore = {};
|
final Map<String, List<StaffMemberModel>> staffByStore = {};
|
||||||
for (StoreModel store in stores) {
|
for (StoreModel store in stores) {
|
||||||
|
|||||||
@@ -81,6 +81,17 @@ class StoreModel extends Equatable {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
factory StoreModel.empty() {
|
||||||
|
return const StoreModel(
|
||||||
|
nome: '',
|
||||||
|
companyId: '',
|
||||||
|
indirizzo: '',
|
||||||
|
cap: '',
|
||||||
|
comune: '',
|
||||||
|
provincia: '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
factory StoreModel.fromMap(Map<String, dynamic> map) {
|
factory StoreModel.fromMap(Map<String, dynamic> map) {
|
||||||
final providersPivotList = map['associated_providers'] as List?;
|
final providersPivotList = map['associated_providers'] as List?;
|
||||||
List<ProviderModel> providers = [];
|
List<ProviderModel> providers = [];
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.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/bloc/store_cubit.dart';
|
||||||
import 'package:flux/features/master_data/store/models/store_model.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/theme/theme.dart';
|
||||||
import 'package:flux/core/widgets/flux_text_field.dart';
|
import 'package:flux/core/widgets/flux_text_field.dart';
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ class _CreateStoreScreenState extends State<CreateStoreScreen> {
|
|||||||
|
|
||||||
/// Funzione magica per copiare i dati dall'azienda salvata in GetIt
|
/// Funzione magica per copiare i dati dall'azienda salvata in GetIt
|
||||||
void _useCompanyAddress() {
|
void _useCompanyAddress() {
|
||||||
final company = context.read<SessionBloc>().state.company;
|
final company = context.read<SessionCubit>().state.company;
|
||||||
if (company != null) {
|
if (company != null) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_indirizzoController.text = company.indirizzo;
|
_indirizzoController.text = company.indirizzo;
|
||||||
@@ -58,7 +58,7 @@ class _CreateStoreScreenState extends State<CreateStoreScreen> {
|
|||||||
|
|
||||||
void _onSave() {
|
void _onSave() {
|
||||||
if (_formKey.currentState!.validate()) {
|
if (_formKey.currentState!.validate()) {
|
||||||
final company = context.read<SessionBloc>().state.company;
|
final company = context.read<SessionCubit>().state.company;
|
||||||
|
|
||||||
if (company == null) {
|
if (company == null) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
@@ -69,7 +69,7 @@ class _CreateStoreScreenState extends State<CreateStoreScreen> {
|
|||||||
|
|
||||||
final store = StoreModel(
|
final store = StoreModel(
|
||||||
nome: _nomeController.text.trim(),
|
nome: _nomeController.text.trim(),
|
||||||
companyId: company.id,
|
companyId: company.id!,
|
||||||
indirizzo: _indirizzoController.text.trim(),
|
indirizzo: _indirizzoController.text.trim(),
|
||||||
cap: _capController.text.trim(),
|
cap: _capController.text.trim(),
|
||||||
comune: _comuneController.text.trim(),
|
comune: _comuneController.text.trim(),
|
||||||
@@ -84,10 +84,10 @@ class _CreateStoreScreenState extends State<CreateStoreScreen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text('Il tuo primo Negozio')),
|
appBar: AppBar(title: const Text('Il tuo primo Negozio')),
|
||||||
body: BlocConsumer<StoreCubit, StoreState>(
|
body: BlocBuilder<StoreCubit, StoreState>(
|
||||||
listener: (context, state) {
|
/* listener: (context, state) {
|
||||||
if (state.status == StoreStatus.success) {
|
if (state.status == StoreStatus.success) {
|
||||||
context.read<SessionBloc>().add(AppStarted());
|
context.read<SessionCubit>().;
|
||||||
}
|
}
|
||||||
if (state.status == StoreStatus.failure) {
|
if (state.status == StoreStatus.failure) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
@@ -96,7 +96,7 @@ class _CreateStoreScreenState extends State<CreateStoreScreen> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
}, */
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.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/core/widgets/flux_text_field.dart';
|
||||||
import 'package:flux/features/master_data/store/bloc/store_cubit.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/features/master_data/store/models/store_model.dart';
|
||||||
@@ -130,10 +130,10 @@ class _StoreFormState extends State<StoreForm> {
|
|||||||
comune: comuneController.text,
|
comune: comuneController.text,
|
||||||
provincia: provinciaController.text,
|
provincia: provinciaController.text,
|
||||||
companyId: context
|
companyId: context
|
||||||
.read<SessionBloc>()
|
.read<SessionCubit>()
|
||||||
.state
|
.state
|
||||||
.company!
|
.company!
|
||||||
.id, // Recuperiamo la companyId
|
.id!, // Recuperiamo la companyId
|
||||||
isActive: widget.store?.isActive ?? true,
|
isActive: widget.store?.isActive ?? true,
|
||||||
isPaid: widget.store?.isPaid ?? false,
|
isPaid: widget.store?.isPaid ?? false,
|
||||||
paymentExpiration: widget.store?.paymentExpiration,
|
paymentExpiration: widget.store?.paymentExpiration,
|
||||||
|
|||||||
105
lib/features/onboarding/blocs/onboarding_cubit.dart
Normal file
105
lib/features/onboarding/blocs/onboarding_cubit.dart
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
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';
|
||||||
|
import 'package:get_it/get_it.dart';
|
||||||
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
|
||||||
|
class OnboardingCubit extends Cubit<OnboardingState> {
|
||||||
|
final CoreRepository _repository;
|
||||||
|
final SessionCubit _sessionCubit;
|
||||||
|
|
||||||
|
OnboardingCubit(this._sessionCubit, this._repository)
|
||||||
|
: super(OnboardingState(
|
||||||
|
step: _sessionCubit.state.onboardingStep,
|
||||||
|
companyId: _sessionCubit.state.company?.id,
|
||||||
|
storeId: _sessionCubit.state.currentStore?.id,
|
||||||
|
));
|
||||||
|
|
||||||
|
// --- STEP 1: REGISTRAZIONE AZIENDA ---
|
||||||
|
Future<void> saveCompany(String companyName) async {
|
||||||
|
emit(state.copyWith(isLoading: true));
|
||||||
|
final company = CompanyModel.empty().copyWith(
|
||||||
|
ragioneSociale: companyName,
|
||||||
|
userId: GetIt.I<SupabaseClient>().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);
|
||||||
|
|
||||||
|
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;
|
||||||
|
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(
|
||||||
|
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) return;
|
||||||
|
if (state.companyId == '') 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!,
|
||||||
|
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];
|
||||||
|
}
|
||||||
85
lib/features/onboarding/ui/company_onboarding_form.dart
Normal file
85
lib/features/onboarding/ui/company_onboarding_form.dart
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
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<CompanyOnboardingForm> createState() => _CompanyOnboardingFormState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CompanyOnboardingFormState extends State<CompanyOnboardingForm> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
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(
|
||||||
|
"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),
|
||||||
|
|
||||||
|
FluxTextField(
|
||||||
|
label: 'Ragione Sociale / Nome Azienda',
|
||||||
|
controller: _nameCtrl,
|
||||||
|
validator: notEmptyValidator,
|
||||||
|
keyboardType: TextInputType.name,
|
||||||
|
textCapitalization: TextCapitalization.words,
|
||||||
|
autocorrect: false,
|
||||||
|
onSubmitted: (_) => _submit(),
|
||||||
|
),
|
||||||
|
|
||||||
|
const Spacer(),
|
||||||
|
ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: () => _submit(),
|
||||||
|
child: const Text(
|
||||||
|
"Salva e Prosegui",
|
||||||
|
style: TextStyle(fontSize: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _submit() {
|
||||||
|
if (_formKey.currentState!.validate()) {
|
||||||
|
context.read<OnboardingCubit>().saveCompany(_nameCtrl.text.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
115
lib/features/onboarding/ui/onboarding_screen.dart
Normal file
115
lib/features/onboarding/ui/onboarding_screen.dart
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
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/onboarding/blocs/onboarding_cubit.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';
|
||||||
|
|
||||||
|
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();
|
||||||
|
// 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();
|
||||||
|
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 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: [
|
||||||
|
CompanyOnboardingForm(state: state),
|
||||||
|
StoreOnboardingForm(state: state),
|
||||||
|
StaffOnboardingForm(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// OVERLAY CARICAMENTO
|
||||||
|
if (state.isLoading)
|
||||||
|
Container(
|
||||||
|
color: Colors.black.withValues(alpha: 0.4),
|
||||||
|
child: const Center(child: CircularProgressIndicator()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
105
lib/features/onboarding/ui/staff_onboarding_form.dart
Normal file
105
lib/features/onboarding/ui/staff_onboarding_form.dart
Normal file
@@ -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<StaffOnboardingForm> createState() => _StaffOnboardingFormState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StaffOnboardingFormState extends State<StaffOnboardingForm> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
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<OnboardingCubit>().saveStaff(newStaff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
179
lib/features/onboarding/ui/store_onboarding_form.dart
Normal file
179
lib/features/onboarding/ui/store_onboarding_form.dart
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
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';
|
||||||
|
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<StoreOnboardingForm> createState() => _StoreOnboardingFormState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StoreOnboardingFormState extends State<StoreOnboardingForm> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
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: 32),
|
||||||
|
|
||||||
|
FluxTextField(
|
||||||
|
controller: _nameCtrl,
|
||||||
|
label: "Nome del Negozio",
|
||||||
|
keyboardType: TextInputType.name,
|
||||||
|
validator: (value) =>
|
||||||
|
value == null || value.isEmpty ? "Obbligatorio" : null,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
FluxTextField(
|
||||||
|
controller: _addressCtrl,
|
||||||
|
keyboardType: TextInputType.streetAddress,
|
||||||
|
label: "Indirizzo",
|
||||||
|
validator: (value) =>
|
||||||
|
value == null || value.isEmpty ? "Obbligatorio" : null,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// IL LAYOUT RESPONSIVO PREMIUM
|
||||||
|
LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
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: [
|
||||||
|
_buildCityField(),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(flex: 3, child: _buildZipField()),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(flex: 2, child: _buildProvField()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
const Spacer(),
|
||||||
|
ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: () => _submit(),
|
||||||
|
child: const Text(
|
||||||
|
"Salva Negozio",
|
||||||
|
style: TextStyle(fontSize: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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<OnboardingCubit>().saveStore(newStore);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 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)],
|
||||||
|
onSubmitted: (_) => _submit(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import 'package:equatable/equatable.dart';
|
|||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.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/core/utils/string_extensions.dart';
|
||||||
import 'package:flux/features/services/data/services_repository.dart';
|
import 'package:flux/features/services/data/services_repository.dart';
|
||||||
import 'package:flux/features/services/models/energy_service_model.dart';
|
import 'package:flux/features/services/models/energy_service_model.dart';
|
||||||
@@ -16,7 +16,7 @@ part 'services_state.dart';
|
|||||||
|
|
||||||
class ServicesCubit extends Cubit<ServicesState> {
|
class ServicesCubit extends Cubit<ServicesState> {
|
||||||
final ServicesRepository _repository = GetIt.I<ServicesRepository>();
|
final ServicesRepository _repository = GetIt.I<ServicesRepository>();
|
||||||
final SessionBloc _sessionBloc = GetIt.I<SessionBloc>();
|
final SessionCubit _sessionCubit = GetIt.I<SessionCubit>();
|
||||||
|
|
||||||
ServicesCubit() : super(const ServicesState(status: ServicesStatus.initial));
|
ServicesCubit() : super(const ServicesState(status: ServicesStatus.initial));
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ class ServicesCubit extends Cubit<ServicesState> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final currentOffset = refresh ? 0 : state.allServices.length;
|
final currentOffset = refresh ? 0 : state.allServices.length;
|
||||||
final companyId = _sessionBloc.state.company?.id;
|
final companyId = _sessionCubit.state.company?.id;
|
||||||
|
|
||||||
if (companyId == null) {
|
if (companyId == null) {
|
||||||
throw Exception("Company ID non trovato nella sessione");
|
throw Exception("Company ID non trovato nella sessione");
|
||||||
@@ -126,10 +126,10 @@ class ServicesCubit extends Cubit<ServicesState> {
|
|||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
currentService: ServiceModel(
|
currentService: ServiceModel(
|
||||||
storeId: _sessionBloc.state.selectedStore?.id ?? '',
|
storeId: _sessionCubit.state.currentStore?.id ?? '',
|
||||||
number: '', // Sarà compilato dall'utente
|
number: '', // Sarà compilato dall'utente
|
||||||
createdAt: DateTime.now(),
|
createdAt: DateTime.now(),
|
||||||
companyId: _sessionBloc.state.company!.id,
|
companyId: _sessionCubit.state.company!.id!,
|
||||||
),
|
),
|
||||||
status: ServicesStatus.ready,
|
status: ServicesStatus.ready,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
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/data/customer_repository.dart';
|
||||||
import 'package:flux/features/customers/models/customer_file_model.dart';
|
import 'package:flux/features/customers/models/customer_file_model.dart';
|
||||||
import 'package:flux/features/services/models/service_file_model.dart';
|
import 'package:flux/features/services/models/service_file_model.dart';
|
||||||
@@ -9,7 +9,7 @@ import '../models/service_model.dart';
|
|||||||
|
|
||||||
class ServicesRepository {
|
class ServicesRepository {
|
||||||
final _supabase = Supabase.instance.client;
|
final _supabase = Supabase.instance.client;
|
||||||
final companyId = GetIt.I.get<SessionBloc>().state.company!.id;
|
final companyId = GetIt.I.get<SessionCubit>().state.company!.id;
|
||||||
final CustomerRepository _customerRepository = GetIt.I<CustomerRepository>();
|
final CustomerRepository _customerRepository = GetIt.I<CustomerRepository>();
|
||||||
|
|
||||||
// --- RECUPERO SINGOLO SERVIZIO CON JOIN COMPLETO ---
|
// --- RECUPERO SINGOLO SERVIZIO CON JOIN COMPLETO ---
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.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/blocs/provider_cubit.dart';
|
||||||
import 'package:flux/features/master_data/providers/models/provider_model.dart';
|
import 'package:flux/features/master_data/providers/models/provider_model.dart';
|
||||||
import 'package:flux/features/services/data/services_repository.dart';
|
import 'package:flux/features/services/data/services_repository.dart';
|
||||||
@@ -281,7 +281,7 @@ class _EntertainmentFormState extends State<_EntertainmentForm> {
|
|||||||
// Suggerimenti rapidi (Chip)
|
// Suggerimenti rapidi (Chip)
|
||||||
FutureBuilder<List<String>>(
|
FutureBuilder<List<String>>(
|
||||||
future: GetIt.I<ServicesRepository>().fetchTopEntertainmentTypes(
|
future: GetIt.I<ServicesRepository>().fetchTopEntertainmentTypes(
|
||||||
GetIt.I<SessionBloc>().state.company!.id,
|
GetIt.I<SessionCubit>().state.company!.id!,
|
||||||
),
|
),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
final suggestions = snapshot.data ?? ["Netflix", "DAZN", "Sky"];
|
final suggestions = snapshot.data ?? ["Netflix", "DAZN", "Sky"];
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.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/master_data/store/bloc/store_cubit.dart';
|
||||||
import 'package:flux/features/services/blocs/services_cubit.dart';
|
import 'package:flux/features/services/blocs/services_cubit.dart';
|
||||||
import 'package:flux/features/services/models/service_model.dart';
|
import 'package:flux/features/services/models/service_model.dart';
|
||||||
@@ -8,8 +8,8 @@ import 'package:go_router/go_router.dart';
|
|||||||
|
|
||||||
/// Avvia la creazione di un nuovo servizio partendo dalla selezione dell'operatore.
|
/// Avvia la creazione di un nuovo servizio partendo dalla selezione dell'operatore.
|
||||||
void startNewService(BuildContext context) {
|
void startNewService(BuildContext context) {
|
||||||
final session = context.read<SessionBloc>().state;
|
final session = context.read<SessionCubit>().state;
|
||||||
final currentStoreId = session.selectedStore?.id;
|
final currentStoreId = session.currentStore?.id;
|
||||||
|
|
||||||
if (currentStoreId == null) {
|
if (currentStoreId == null) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
@@ -59,7 +59,7 @@ void startNewService(BuildContext context) {
|
|||||||
employeeId: member.id,
|
employeeId: member.id,
|
||||||
number: '',
|
number: '',
|
||||||
createdAt: DateTime.now(),
|
createdAt: DateTime.now(),
|
||||||
companyId: session.company!.id,
|
companyId: session.company!.id!,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||||
import 'package:flux/core/blocs/session/session_bloc.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';
|
||||||
|
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/routes/app_router.dart';
|
||||||
import 'package:flux/core/theme/theme.dart';
|
import 'package:flux/core/theme/theme.dart';
|
||||||
import 'package:flux/core/theme/bloc/theme_bloc.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/blocs/customer_cubit.dart';
|
||||||
import 'package:flux/features/customers/data/customer_repository.dart';
|
import 'package:flux/features/customers/data/customer_repository.dart';
|
||||||
import 'package:flux/features/master_data/products/blocs/product_cubit.dart';
|
import 'package:flux/features/master_data/products/blocs/product_cubit.dart';
|
||||||
@@ -21,33 +24,31 @@ 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/blocs/services_cubit.dart';
|
||||||
import 'package:flux/features/services/data/services_repository.dart';
|
import 'package:flux/features/services/data/services_repository.dart';
|
||||||
import 'package:flux/features/settings/settings.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 {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
await dotenv.load(fileName: ".env");
|
await dotenv.load(fileName: ".env");
|
||||||
|
|
||||||
|
// Inizializza le dipendenze PRIMA di lanciare l'app
|
||||||
await setupLocator();
|
await setupLocator();
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
MultiBlocProvider(
|
MultiBlocProvider(
|
||||||
providers: [
|
providers: [
|
||||||
|
BlocProvider<AuthCubit>(create: (context) => AuthCubit()),
|
||||||
BlocProvider<ThemeBloc>(
|
BlocProvider<ThemeBloc>(
|
||||||
create: (context) => ThemeBloc()..add(LoadThemeEvent()),
|
create: (context) => ThemeBloc()..add(LoadThemeEvent()),
|
||||||
),
|
),
|
||||||
BlocProvider<SessionBloc>(create: (_) => GetIt.I<SessionBloc>()),
|
// Il Vigile Urbano viene inizializzato!
|
||||||
BlocProvider<AuthBloc>(create: (_) => AuthBloc()),
|
BlocProvider<SessionCubit>(create: (_) => GetIt.I<SessionCubit>()),
|
||||||
BlocProvider<CompanyBloc>(create: (_) => CompanyBloc()),
|
|
||||||
BlocProvider<StoreCubit>(create: (_) => StoreCubit()..loadStores()),
|
// Cubit delle feature
|
||||||
|
BlocProvider<StoreCubit>(create: (_) => StoreCubit()),
|
||||||
BlocProvider<CustomerCubit>(create: (_) => CustomerCubit()),
|
BlocProvider<CustomerCubit>(create: (_) => CustomerCubit()),
|
||||||
BlocProvider<ProductCubit>(create: (_) => ProductCubit()),
|
BlocProvider<ProductCubit>(create: (_) => ProductCubit()),
|
||||||
BlocProvider<StaffCubit>(create: (_) => StaffCubit()..loadAllStaff()),
|
BlocProvider<StaffCubit>(create: (_) => StaffCubit()),
|
||||||
BlocProvider<ServicesCubit>(create: (_) => ServicesCubit()),
|
BlocProvider<ServicesCubit>(create: (_) => ServicesCubit()),
|
||||||
BlocProvider<ProvidersCubit>(
|
BlocProvider<ProvidersCubit>(create: (_) => ProvidersCubit()),
|
||||||
create: (_) => ProvidersCubit()..loadProviders(null),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
child: const FluxApp(),
|
child: const FluxApp(),
|
||||||
),
|
),
|
||||||
@@ -56,6 +57,7 @@ void main() async {
|
|||||||
|
|
||||||
Future<void> setupLocator() async {
|
Future<void> setupLocator() async {
|
||||||
final GetIt getIt = GetIt.instance;
|
final GetIt getIt = GetIt.instance;
|
||||||
|
|
||||||
getIt.registerSingleton<SharedPreferences>(
|
getIt.registerSingleton<SharedPreferences>(
|
||||||
await SharedPreferences.getInstance(),
|
await SharedPreferences.getInstance(),
|
||||||
);
|
);
|
||||||
@@ -64,16 +66,32 @@ Future<void> setupLocator() async {
|
|||||||
url: dotenv.env['SUPABASE_URL'] ?? '',
|
url: dotenv.env['SUPABASE_URL'] ?? '',
|
||||||
anonKey: dotenv.env['SUPABASE_ANON_KEY'] ?? '',
|
anonKey: dotenv.env['SUPABASE_ANON_KEY'] ?? '',
|
||||||
);
|
);
|
||||||
|
//await Supabase.instance.client.auth.signOut();
|
||||||
getIt.registerSingleton<SupabaseClient>(Supabase.instance.client);
|
getIt.registerSingleton<SupabaseClient>(Supabase.instance.client);
|
||||||
|
|
||||||
|
// Settings
|
||||||
getIt.registerLazySingleton<AppSettings>(() => AppSettings());
|
getIt.registerLazySingleton<AppSettings>(() => AppSettings());
|
||||||
getIt.registerLazySingleton<CompanyRepository>(() => CompanyRepository());
|
|
||||||
|
// Repositories
|
||||||
|
getIt.registerLazySingleton<CoreRepository>(
|
||||||
|
() => CoreRepository(),
|
||||||
|
); // <-- NUOVO
|
||||||
getIt.registerLazySingleton<StoreRepository>(() => StoreRepository());
|
getIt.registerLazySingleton<StoreRepository>(() => StoreRepository());
|
||||||
getIt.registerLazySingleton<CustomerRepository>(() => CustomerRepository());
|
getIt.registerLazySingleton<CustomerRepository>(() => CustomerRepository());
|
||||||
getIt.registerLazySingleton<ProductRepository>(() => ProductRepository());
|
getIt.registerLazySingleton<ProductRepository>(() => ProductRepository());
|
||||||
getIt.registerLazySingleton<StaffRepository>(() => StaffRepository());
|
getIt.registerLazySingleton<StaffRepository>(() => StaffRepository());
|
||||||
getIt.registerLazySingleton<ServicesRepository>(() => ServicesRepository());
|
getIt.registerLazySingleton<ServicesRepository>(() => ServicesRepository());
|
||||||
getIt.registerLazySingleton<ProviderRepository>(() => ProviderRepository());
|
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 {
|
class FluxApp extends StatefulWidget {
|
||||||
@@ -89,26 +107,27 @@ class _FluxAppState extends State<FluxApp> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
// Lo creiamo una volta sola all'avvio dell'app
|
// Creiamo il router passandogli il Cubit per i redirect
|
||||||
_router = AppRouter.createRouter(context.read<SessionBloc>());
|
_router = AppRouter.createRouter(context.read<SessionCubit>());
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocBuilder<SessionBloc, SessionState>(
|
return BlocBuilder<SessionCubit, SessionState>(
|
||||||
builder: (context, state) {
|
builder: (context, sessionState) {
|
||||||
if (state.status == SessionStatus.unknown) {
|
if (sessionState.status == SessionStatus.initial) {
|
||||||
return _buildLoadingScreen();
|
return _buildLoadingScreen();
|
||||||
}
|
}
|
||||||
|
|
||||||
return BlocBuilder<ThemeBloc, ThemeState>(
|
return BlocBuilder<ThemeBloc, ThemeState>(
|
||||||
builder: (context, state) {
|
builder: (context, themeState) {
|
||||||
return MaterialApp.router(
|
return MaterialApp.router(
|
||||||
title: 'FLUX Gestionale',
|
title: 'FLUX Gestionale',
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
theme: fluxLightTheme,
|
theme: fluxLightTheme,
|
||||||
darkTheme: fluxDarkTheme,
|
darkTheme: fluxDarkTheme,
|
||||||
themeMode: state.currentTheme.themeMode,
|
themeMode: themeState.currentTheme.themeMode,
|
||||||
routerConfig: _router, // Usa l'istanza mantenuta nello stato
|
routerConfig: _router,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -116,7 +135,6 @@ class _FluxAppState extends State<FluxApp> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Una semplice schermata di caricamento coerente con il brand
|
|
||||||
Widget _buildLoadingScreen() {
|
Widget _buildLoadingScreen() {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
@@ -125,7 +143,6 @@ class _FluxAppState extends State<FluxApp> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
// Qui puoi mettere il tuo logo
|
|
||||||
const Icon(Icons.bolt, size: 64, color: Colors.blue),
|
const Icon(Icons.bolt, size: 64, color: Colors.blue),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
const CircularProgressIndicator(),
|
const CircularProgressIndicator(),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
jni
|
||||||
)
|
)
|
||||||
|
|
||||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ PODS:
|
|||||||
- file_picker (0.0.1):
|
- file_picker (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- FlutterMacOS (1.0.0)
|
- FlutterMacOS (1.0.0)
|
||||||
|
- pdfx (1.0.0):
|
||||||
|
- FlutterMacOS
|
||||||
- shared_preferences_foundation (0.0.1):
|
- shared_preferences_foundation (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
@@ -14,6 +16,7 @@ DEPENDENCIES:
|
|||||||
- app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`)
|
- app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`)
|
||||||
- file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`)
|
- file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`)
|
||||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||||
|
- pdfx (from `Flutter/ephemeral/.symlinks/plugins/pdfx/macos`)
|
||||||
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
|
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||||
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
- 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
|
:path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos
|
||||||
FlutterMacOS:
|
FlutterMacOS:
|
||||||
:path: Flutter/ephemeral
|
:path: Flutter/ephemeral
|
||||||
|
pdfx:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/pdfx/macos
|
||||||
shared_preferences_foundation:
|
shared_preferences_foundation:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
|
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
|
||||||
url_launcher_macos:
|
url_launcher_macos:
|
||||||
@@ -33,6 +38,7 @@ SPEC CHECKSUMS:
|
|||||||
app_links: 05a6ec2341985eb05e9f97dc63f5837c39895c3f
|
app_links: 05a6ec2341985eb05e9f97dc63f5837c39895c3f
|
||||||
file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a
|
file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a
|
||||||
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
|
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
|
||||||
|
pdfx: 1e79f57f7a6ce2f4a4c30f21fa54d3dc82441b51
|
||||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||||
url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd
|
url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd
|
||||||
|
|
||||||
|
|||||||
68
pubspec.lock
68
pubspec.lock
@@ -125,10 +125,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: dart_jsonwebtoken
|
name: dart_jsonwebtoken
|
||||||
sha256: cb79ed79baa02b4f59a597bf365873cbd83f9bb15273d63f7803802d21717c7d
|
sha256: ad84e60181696513d04d5f2078e0bbc20365b911f46f647797317414bdc88fbe
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.4.0"
|
version: "3.4.1"
|
||||||
dbus:
|
dbus:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -210,10 +210,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_dotenv
|
name: flutter_dotenv
|
||||||
sha256: d4130c4a43e0b13fefc593bc3961f2cb46e30cb79e253d4a526b1b5d24ae1ce4
|
sha256: d41da11fb497314fbf89811ec30af02d1d898b47980a129f0a8c0a1720460ba2
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.0"
|
version: "6.0.1"
|
||||||
flutter_lints:
|
flutter_lints:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@@ -276,10 +276,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: go_router
|
name: go_router
|
||||||
sha256: "48fb2f42ad057476fa4b733cb95e9f9ea7b0b010bb349ea491dca7dbdb18ffc4"
|
sha256: "08b742eef4f71c9df5af543751cd0b7f1c679c4088488f4223ecaddc1a813b79"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "17.2.0"
|
version: "17.2.2"
|
||||||
google_fonts:
|
google_fonts:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -292,10 +292,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: gotrue
|
name: gotrue
|
||||||
sha256: ecdf3fa3ef8c5f886390ba0056d00d29138c02c39984e9caa8194dffd8a73ef7
|
sha256: "7a4172601553e61716f5c3dd243aa3297e13308e07eb85b7853c941ba585dcf5"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.19.0"
|
version: "2.20.0"
|
||||||
gtk:
|
gtk:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -344,6 +344,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.20.2"
|
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:
|
jwt_decode:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -448,6 +464,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "9.3.0"
|
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:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -476,10 +500,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_android
|
name: path_provider_android
|
||||||
sha256: "149441ca6e4f38193b2e004c0ca6376a3d11f51fa5a77552d8bd4d2b0c0912ba"
|
sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.23"
|
version: "2.3.1"
|
||||||
path_provider_foundation:
|
path_provider_foundation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -564,10 +588,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: postgrest
|
name: postgrest
|
||||||
sha256: f4b6bb24b465c47649243ef0140475de8a0ec311dc9c75ebe573b2dcabb10460
|
sha256: "9d61b3d4a88fcf9424d400127c54d49ed1b56ec30838fc0a33a64f31d4e694cc"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.6.0"
|
version: "2.7.0"
|
||||||
provider:
|
provider:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -588,10 +612,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: realtime_client
|
name: realtime_client
|
||||||
sha256: ee8e71af7a834e960f5b2f494f398117488036fbdb11f422611f7287fbf40562
|
sha256: "7dfccf372d2f55aacfeefb6186f65a06f3ffae383fe042dbeef9d85d33487576"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.7.1"
|
version: "2.7.3"
|
||||||
retry:
|
retry:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -689,10 +713,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: storage_client
|
name: storage_client
|
||||||
sha256: "085a08fd67f234d575113957c04a0e8d0a3050129762f939ce831ee2c0df8257"
|
sha256: "4801e8ca219a35e51cbb30589aba5306667ae8935b792504595a45273cef0b18"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.5.1"
|
version: "2.5.2"
|
||||||
stream_channel:
|
stream_channel:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -713,18 +737,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: supabase
|
name: supabase
|
||||||
sha256: "89b190b585f8609fe1537cbf53eae0c9fda9b777591b064d1150c6f26e607a84"
|
sha256: "40e5a8833c8834e140ef53b60a6181849667eba9ca125acb7f8e24c6a769d418"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.10.4"
|
version: "2.10.6"
|
||||||
supabase_flutter:
|
supabase_flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: supabase_flutter
|
name: supabase_flutter
|
||||||
sha256: c2974cfdfeb5de517652a35f3ef0d1f3159e068de82b50ccaa27908a2b45fb82
|
sha256: c02ce58abcaf86cb8055ad40bfd98bbf5b93fed3b5b56b8220d88ed03842818b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.12.2"
|
version: "2.12.4"
|
||||||
synchronized:
|
synchronized:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -881,10 +905,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vm_service
|
name: vm_service
|
||||||
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
|
sha256: "046d3928e16fa4dc46e8350415661755ab759d9fc97fc21b5ab295f71e4f0499"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "15.0.2"
|
version: "15.1.0"
|
||||||
web:
|
web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
jni
|
||||||
)
|
)
|
||||||
|
|
||||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||||
|
|||||||
Reference in New Issue
Block a user