rework-onboarding #7

Merged
brontomark merged 6 commits from rework-onboarding into main 2026-04-22 11:06:02 +02:00
13 changed files with 211 additions and 120 deletions
Showing only changes of commit 46058d96c8 - Show all commits

31
.vscode/launch.json vendored Normal file
View 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"]
}
]
}

View File

@@ -49,6 +49,8 @@ class SessionCubit extends Cubit<SessionState> {
onboardingStep: OnboardingStep.company, onboardingStep: OnboardingStep.company,
), ),
); );
} else {
emit(state.copyWith(company: company));
} }
// 2. Controllo Negozi // 2. Controllo Negozi
@@ -62,6 +64,8 @@ class SessionCubit extends Cubit<SessionState> {
onboardingStep: OnboardingStep.store, onboardingStep: OnboardingStep.store,
), ),
); );
} else {
emit(state.copyWith(currentStore: stores.first));
} }
// 3. Controllo Staff (Paziente Zero) // 3. Controllo Staff (Paziente Zero)

View File

@@ -1,6 +1,9 @@
import 'package:flutter/foundation.dart';
import 'package:flux/core/blocs/session/session_cubit.dart';
import 'package:flux/features/company/models/company_model.dart'; import 'package:flux/features/company/models/company_model.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/features/master_data/staff/models/staff_member_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... // Importa i tuoi modelli...
import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:supabase_flutter/supabase_flutter.dart';
@@ -20,6 +23,7 @@ class CoreRepository {
if (response == null) return null; if (response == null) return null;
return CompanyModel.fromMap(response); return CompanyModel.fromMap(response);
} catch (e) { } catch (e) {
debugPrint('Errore recupero azienda: $e');
throw Exception('Errore recupero azienda: $e'); throw Exception('Errore recupero azienda: $e');
} }
} }
@@ -35,6 +39,7 @@ class CoreRepository {
return (response as List).map((s) => StoreModel.fromMap(s)).toList(); return (response as List).map((s) => StoreModel.fromMap(s)).toList();
} catch (e) { } catch (e) {
debugPrint('Errore recupero negozi: $e');
throw Exception('Errore recupero negozi: $e'); throw Exception('Errore recupero negozi: $e');
} }
} }
@@ -50,6 +55,7 @@ class CoreRepository {
if (response == null) return null; if (response == null) return null;
return StaffMemberModel.fromMap(response); return StaffMemberModel.fromMap(response);
} catch (e) { } catch (e) {
debugPrint('Errore recupero profilo staff: $e');
throw Exception('Errore recupero profilo staff: $e'); throw Exception('Errore recupero profilo staff: $e');
} }
} }
@@ -65,6 +71,7 @@ class CoreRepository {
.single(); .single();
return CompanyModel.fromMap(response); return CompanyModel.fromMap(response);
} catch (e) { } catch (e) {
debugPrint('Creazione azienda fallita: $e');
throw Exception('Creazione azienda fallita: $e'); throw Exception('Creazione azienda fallita: $e');
} }
} }
@@ -78,6 +85,7 @@ class CoreRepository {
.single(); .single();
return StoreModel.fromMap(response); return StoreModel.fromMap(response);
} catch (e) { } catch (e) {
debugPrint('Creazione negozio fallita: $e');
throw Exception('Creazione negozio fallita: $e'); throw Exception('Creazione negozio fallita: $e');
} }
} }
@@ -89,8 +97,14 @@ class CoreRepository {
.insert(staff.toMap()) .insert(staff.toMap())
.select() .select()
.single(); .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); return StaffMemberModel.fromMap(response);
} catch (e) { } catch (e) {
debugPrint('Creazione profilo staff fallita: $e');
throw Exception('Creazione profilo staff fallita: $e'); throw Exception('Creazione profilo staff fallita: $e');
} }
} }

View File

@@ -19,6 +19,7 @@ class FluxTextField extends StatefulWidget {
final String? Function(String?)? validator; final String? Function(String?)? validator;
final List<TextInputFormatter>? inputFormatters; final List<TextInputFormatter>? inputFormatters;
final TextCapitalization? textCapitalization; 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
@@ -37,6 +38,7 @@ class FluxTextField extends StatefulWidget {
this.validator, this.validator,
this.inputFormatters, this.inputFormatters,
this.textCapitalization, this.textCapitalization,
this.autocorrect,
}); });
@override @override
@@ -58,8 +60,9 @@ class _FluxTextFieldState extends State<FluxTextField> {
controller: widget.controller, controller: widget.controller,
validator: widget.validator, 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,
@@ -110,6 +113,7 @@ class _FluxTextFieldState extends State<FluxTextField> {
onChanged: widget.onChanged, onChanged: widget.onChanged,
maxLength: widget.maxLength, maxLength: widget.maxLength,
inputFormatters: widget.inputFormatters, inputFormatters: widget.inputFormatters,
textCapitalization: widget.textCapitalization ?? TextCapitalization.none, textCapitalization: widget.textCapitalization ?? TextCapitalization.none,
); );
} }

View File

@@ -63,4 +63,9 @@ class AuthCubit extends Cubit<AuthState> {
); );
} }
} }
Future<void> requestLogout() async {
await _supabase.auth.signOut();
emit(state.copyWith(status: AuthStatus.initial));
}
} }

View File

@@ -100,7 +100,7 @@ class _AuthScreenState extends State<AuthScreen> {
label: 'Email Aziendale', label: 'Email Aziendale',
icon: Icons.email_outlined, icon: Icons.email_outlined,
controller: _emailController, controller: _emailController,
// TODO: Aggiungi nel tuo FluxTextField la gestione del keyboardType se non c'è già! keyboardType: TextInputType.emailAddress,
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
FluxTextField( FluxTextField(
@@ -108,7 +108,8 @@ class _AuthScreenState extends State<AuthScreen> {
icon: Icons.lock_outline, icon: Icons.lock_outline,
isPassword: true, // Magia del FluxTextField! isPassword: true, // Magia del FluxTextField!
controller: _passwordController, controller: _passwordController,
// onSubmitted: (_) => _submit(), // Se lo supporti nel tuo widget custom onSubmitted: (_) =>
_submit(), // Se lo supporti nel tuo widget custom
), ),
const SizedBox(height: 40), const SizedBox(height: 40),

View File

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

View File

@@ -13,7 +13,11 @@ class OnboardingCubit extends Cubit<OnboardingState> {
final SessionCubit _sessionCubit; final SessionCubit _sessionCubit;
OnboardingCubit(this._sessionCubit, this._repository) OnboardingCubit(this._sessionCubit, this._repository)
: super(OnboardingState(step: _sessionCubit.state.onboardingStep)); : super(OnboardingState(
step: _sessionCubit.state.onboardingStep,
companyId: _sessionCubit.state.company?.id,
storeId: _sessionCubit.state.currentStore?.id,
));
// --- STEP 1: REGISTRAZIONE AZIENDA --- // --- STEP 1: REGISTRAZIONE AZIENDA ---
Future<void> saveCompany(String companyName) async { Future<void> saveCompany(String companyName) async {
@@ -49,12 +53,14 @@ class OnboardingCubit extends Cubit<OnboardingState> {
// --- STEP 2: REGISTRAZIONE PRIMO NEGOZIO --- // --- STEP 2: REGISTRAZIONE PRIMO NEGOZIO ---
Future<void> saveStore(StoreModel store) async { Future<void> saveStore(StoreModel store) async {
if (state.companyId == null) return; if (state.companyId == null) return;
if (state.companyId == '') return;
emit(state.copyWith(isLoading: true)); emit(state.copyWith(isLoading: true));
try { try {
// Iniettiamo forzatamente il companyId ottenuto dallo step precedente // Iniettiamo forzatamente il companyId ottenuto dallo step precedente
final storeToSave = store.copyWith(companyId: state.companyId); final storeToSave = store.copyWith(companyId: state.companyId);
final savedStore = await _repository.createStore(storeToSave); final savedStore = await _repository.createStore(storeToSave);
_sessionCubit.changeStore(savedStore);
emit( emit(
state.copyWith( state.copyWith(
@@ -72,7 +78,8 @@ class OnboardingCubit extends Cubit<OnboardingState> {
// --- STEP 3: REGISTRAZIONE PROFILO STAFF (PAZIENTE ZERO) --- // --- STEP 3: REGISTRAZIONE PROFILO STAFF (PAZIENTE ZERO) ---
Future<void> saveStaff(StaffMemberModel staff) async { Future<void> saveStaff(StaffMemberModel staff) async {
if (state.companyId == null || state.storeId == null) return; if (state.companyId == null) return;
if (state.companyId == '') return;
emit(state.copyWith(isLoading: true)); emit(state.copyWith(isLoading: true));
try { try {

View File

@@ -50,6 +50,10 @@ class _CompanyOnboardingFormState extends State<CompanyOnboardingForm> {
label: 'Ragione Sociale / Nome Azienda', label: 'Ragione Sociale / Nome Azienda',
controller: _nameCtrl, controller: _nameCtrl,
validator: notEmptyValidator, validator: notEmptyValidator,
keyboardType: TextInputType.name,
textCapitalization: TextCapitalization.words,
autocorrect: false,
onSubmitted: (_) => _submit(),
), ),
const Spacer(), const Spacer(),
@@ -60,13 +64,7 @@ class _CompanyOnboardingFormState extends State<CompanyOnboardingForm> {
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
), ),
onPressed: () { onPressed: () => _submit(),
if (_formKey.currentState!.validate()) {
context.read<OnboardingCubit>().saveCompany(
_nameCtrl.text.trim(),
);
}
},
child: const Text( child: const Text(
"Salva e Prosegui", "Salva e Prosegui",
style: TextStyle(fontSize: 16), style: TextStyle(fontSize: 16),
@@ -78,4 +76,10 @@ class _CompanyOnboardingFormState extends State<CompanyOnboardingForm> {
), ),
); );
} }
void _submit() {
if (_formKey.currentState!.validate()) {
context.read<OnboardingCubit>().saveCompany(_nameCtrl.text.trim());
}
}
} }

View File

@@ -1,18 +1,11 @@
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_cubit.dart'; import 'package:flux/core/blocs/session/session_cubit.dart';
import 'package:flux/core/data/core_repository.dart';
import 'package:flux/core/utils/validators.dart';
import 'package:flux/features/master_data/store/models/store_model.dart';
import 'package:flux/features/master_data/staff/models/staff_member_model.dart';
import 'package:flux/features/onboarding/blocs/onboarding_cubit.dart'; import 'package:flux/features/onboarding/blocs/onboarding_cubit.dart';
// Sostituisci con il percorso corretto della tua FluxTextField
import 'package:flux/core/widgets/flux_text_field.dart';
import 'package:flux/features/onboarding/blocs/onboarding_state.dart'; import 'package:flux/features/onboarding/blocs/onboarding_state.dart';
import 'package:flux/features/onboarding/ui/company_onboarding_form.dart'; import 'package:flux/features/onboarding/ui/company_onboarding_form.dart';
import 'package:flux/features/onboarding/ui/staff_onboarding_form.dart';
import 'package:flux/features/onboarding/ui/store_onboarding_form.dart'; import 'package:flux/features/onboarding/ui/store_onboarding_form.dart';
import 'package:get_it/get_it.dart';
class OnboardingScreen extends StatefulWidget { class OnboardingScreen extends StatefulWidget {
const OnboardingScreen({super.key}); const OnboardingScreen({super.key});
@@ -24,15 +17,6 @@ class OnboardingScreen extends StatefulWidget {
class _OnboardingScreenState extends State<OnboardingScreen> { class _OnboardingScreenState extends State<OnboardingScreen> {
late PageController _pageController; late PageController _pageController;
// --- CHIAVI DEI FORM (Per la validazione indipendente di ogni step) ---
final _staffFormKey = GlobalKey<FormState>();
// --- CONTROLLERS: STEP 3 (Staff) ---
final _staffFirstNameCtrl = TextEditingController();
final _staffLastNameCtrl = TextEditingController();
final _staffJobTitleCtrl = TextEditingController();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -44,9 +28,6 @@ class _OnboardingScreenState extends State<OnboardingScreen> {
@override @override
void dispose() { void dispose() {
_pageController.dispose(); _pageController.dispose();
_staffFirstNameCtrl.dispose();
_staffLastNameCtrl.dispose();
_staffJobTitleCtrl.dispose();
super.dispose(); super.dispose();
} }
@@ -112,9 +93,9 @@ class _OnboardingScreenState extends State<OnboardingScreen> {
physics: physics:
const NeverScrollableScrollPhysics(), // Vietato lo swipe manuale! const NeverScrollableScrollPhysics(), // Vietato lo swipe manuale!
children: [ children: [
CompanyOnboardingForm(state: state), // Step 1: Company CompanyOnboardingForm(state: state),
StoreOnboardingForm(state: state), StoreOnboardingForm(state: state),
_buildStaffForm(context, state), StaffOnboardingForm(),
], ],
), ),
@@ -131,73 +112,4 @@ class _OnboardingScreenState extends State<OnboardingScreen> {
}, },
); );
} }
Widget _buildStaffForm(BuildContext context, OnboardingState state) {
return Padding(
padding: const EdgeInsets.all(32.0),
child: Form(
key: _staffFormKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
"Il tuo Profilo 👤",
style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text(
"Ultimo step! Crea il tuo profilo operativo per iniziare a usare FLUX.",
style: TextStyle(fontSize: 16, color: Colors.grey),
),
const SizedBox(height: 48),
FluxTextField(
label: 'Nome',
controller: _staffFirstNameCtrl,
validator: notEmptyValidator,
),
const SizedBox(height: 16),
FluxTextField(
label: 'Cognome',
controller: _staffLastNameCtrl,
validator: notEmptyValidator,
),
const SizedBox(height: 16),
FluxTextField(
label: 'Etichetta Ruolo (es. Titolare, Manager)',
controller: _staffJobTitleCtrl,
// Il jobTitle può anche essere opzionale, decidi tu!
),
const Spacer(),
ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
backgroundColor: Colors.black, // O il tuo context.accent
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
onPressed: () {
if (_staffFormKey.currentState!.validate()) {
final newStaff = StaffMemberModel.empty().copyWith(
name: _staffFirstNameCtrl.text.trim(),
jobTitle: _staffJobTitleCtrl.text.trim(),
);
context.read<OnboardingCubit>().saveStaff(newStaff);
}
},
child: const Text(
"Entra in FLUX",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
),
const SizedBox(height: 16),
],
),
),
);
}
} }

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

View File

@@ -56,6 +56,7 @@ class _StoreOnboardingFormState extends State<StoreOnboardingForm> {
FluxTextField( FluxTextField(
controller: _nameCtrl, controller: _nameCtrl,
label: "Nome del Negozio", label: "Nome del Negozio",
keyboardType: TextInputType.name,
validator: (value) => validator: (value) =>
value == null || value.isEmpty ? "Obbligatorio" : null, value == null || value.isEmpty ? "Obbligatorio" : null,
), ),
@@ -63,6 +64,7 @@ class _StoreOnboardingFormState extends State<StoreOnboardingForm> {
FluxTextField( FluxTextField(
controller: _addressCtrl, controller: _addressCtrl,
keyboardType: TextInputType.streetAddress,
label: "Indirizzo", label: "Indirizzo",
validator: (value) => validator: (value) =>
value == null || value.isEmpty ? "Obbligatorio" : null, value == null || value.isEmpty ? "Obbligatorio" : null,
@@ -115,7 +117,20 @@ class _StoreOnboardingFormState extends State<StoreOnboardingForm> {
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
), ),
onPressed: () { onPressed: () => _submit(),
child: const Text(
"Salva Negozio",
style: TextStyle(fontSize: 16),
),
),
const SizedBox(height: 16),
],
),
),
);
}
void _submit() {
if (_formKey.currentState!.validate()) { if (_formKey.currentState!.validate()) {
// MIRACOLO DELLA FACTORY EMPTY! // MIRACOLO DELLA FACTORY EMPTY!
final newStore = StoreModel.empty().copyWith( final newStore = StoreModel.empty().copyWith(
@@ -128,17 +143,6 @@ class _StoreOnboardingFormState extends State<StoreOnboardingForm> {
); );
context.read<OnboardingCubit>().saveStore(newStore); context.read<OnboardingCubit>().saveStore(newStore);
} }
},
child: const Text(
"Salva Negozio",
style: TextStyle(fontSize: 16),
),
),
const SizedBox(height: 16),
],
),
),
);
} }
// --- WIDGET ESTRATTI PER PULIZIA --- // --- WIDGET ESTRATTI PER PULIZIA ---
@@ -169,6 +173,7 @@ class _StoreOnboardingFormState extends State<StoreOnboardingForm> {
// Rende la tastiera del telefono automaticamente maiuscola // Rende la tastiera del telefono automaticamente maiuscola
textCapitalization: TextCapitalization.characters, textCapitalization: TextCapitalization.characters,
inputFormatters: [LengthLimitingTextInputFormatter(2)], inputFormatters: [LengthLimitingTextInputFormatter(2)],
onSubmitted: (_) => _submit(),
); );
} }
} }

View File

@@ -66,6 +66,7 @@ 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 // Settings