diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..1fb6a4c --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,31 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "flux", + "request": "launch", + "type": "dart" + }, + { + "name": "s25", + "request":"launch", + "type":"dart", + "deviceId": "RFCY51YEK1N" + }, + { + "name":"mac", + "request":"launch", + "type":"dart", + "deviceId": "macos" + } + ], + "compounds": [ + { + "name": "Compound", + "configurations": ["s25","mac"] + } + ] +} \ No newline at end of file diff --git a/lib/core/blocs/session/session_cubit.dart b/lib/core/blocs/session/session_cubit.dart index 6b8b592..e3f0f70 100644 --- a/lib/core/blocs/session/session_cubit.dart +++ b/lib/core/blocs/session/session_cubit.dart @@ -49,6 +49,8 @@ class SessionCubit extends Cubit { onboardingStep: OnboardingStep.company, ), ); + } else { + emit(state.copyWith(company: company)); } // 2. Controllo Negozi @@ -62,6 +64,8 @@ class SessionCubit extends Cubit { onboardingStep: OnboardingStep.store, ), ); + } else { + emit(state.copyWith(currentStore: stores.first)); } // 3. Controllo Staff (Paziente Zero) diff --git a/lib/core/data/core_repository.dart b/lib/core/data/core_repository.dart index 3eed0b6..eebec0f 100644 --- a/lib/core/data/core_repository.dart +++ b/lib/core/data/core_repository.dart @@ -1,6 +1,9 @@ +import 'package:flutter/foundation.dart'; +import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/features/company/models/company_model.dart'; import 'package:flux/features/master_data/store/models/store_model.dart'; import 'package:flux/features/master_data/staff/models/staff_member_model.dart'; +import 'package:get_it/get_it.dart'; // Importa i tuoi modelli... import 'package:supabase_flutter/supabase_flutter.dart'; @@ -20,6 +23,7 @@ class CoreRepository { if (response == null) return null; return CompanyModel.fromMap(response); } catch (e) { + debugPrint('Errore recupero azienda: $e'); throw Exception('Errore recupero azienda: $e'); } } @@ -35,6 +39,7 @@ class CoreRepository { return (response as List).map((s) => StoreModel.fromMap(s)).toList(); } catch (e) { + debugPrint('Errore recupero negozi: $e'); throw Exception('Errore recupero negozi: $e'); } } @@ -50,6 +55,7 @@ class CoreRepository { if (response == null) return null; return StaffMemberModel.fromMap(response); } catch (e) { + debugPrint('Errore recupero profilo staff: $e'); throw Exception('Errore recupero profilo staff: $e'); } } @@ -65,6 +71,7 @@ class CoreRepository { .single(); return CompanyModel.fromMap(response); } catch (e) { + debugPrint('Creazione azienda fallita: $e'); throw Exception('Creazione azienda fallita: $e'); } } @@ -78,6 +85,7 @@ class CoreRepository { .single(); return StoreModel.fromMap(response); } catch (e) { + debugPrint('Creazione negozio fallita: $e'); throw Exception('Creazione negozio fallita: $e'); } } @@ -89,8 +97,14 @@ class CoreRepository { .insert(staff.toMap()) .select() .single(); + final StaffMemberModel staffMember = StaffMemberModel.fromMap(response); + await _supabase.from('staff_in_stores').insert({ + 'staff_member_id': staffMember.id, + 'store_id': GetIt.I.get().state.currentStore!.id, + }); return StaffMemberModel.fromMap(response); } catch (e) { + debugPrint('Creazione profilo staff fallita: $e'); throw Exception('Creazione profilo staff fallita: $e'); } } diff --git a/lib/core/widgets/flux_text_field.dart b/lib/core/widgets/flux_text_field.dart index 0c72498..a9bafae 100644 --- a/lib/core/widgets/flux_text_field.dart +++ b/lib/core/widgets/flux_text_field.dart @@ -19,6 +19,7 @@ class FluxTextField extends StatefulWidget { final String? Function(String?)? validator; final List? inputFormatters; final TextCapitalization? textCapitalization; + final bool? autocorrect; const FluxTextField({ super.key, // Usiamo super.key per Flutter moderno @@ -37,6 +38,7 @@ class FluxTextField extends StatefulWidget { this.validator, this.inputFormatters, this.textCapitalization, + this.autocorrect, }); @override @@ -58,8 +60,9 @@ class _FluxTextFieldState extends State { controller: widget.controller, validator: widget.validator, obscureText: _obscureText, + enableSuggestions: !widget.isPassword, - autocorrect: !widget.isPassword, + autocorrect: widget.isPassword ? false : widget.autocorrect ?? true, keyboardType: widget.keyboardType, autofocus: widget.autoFocus, minLines: widget.minLines, @@ -110,6 +113,7 @@ class _FluxTextFieldState extends State { onChanged: widget.onChanged, maxLength: widget.maxLength, inputFormatters: widget.inputFormatters, + textCapitalization: widget.textCapitalization ?? TextCapitalization.none, ); } diff --git a/lib/features/auth/bloc/auth_cubit.dart b/lib/features/auth/bloc/auth_cubit.dart index 95e9b56..6f062fe 100644 --- a/lib/features/auth/bloc/auth_cubit.dart +++ b/lib/features/auth/bloc/auth_cubit.dart @@ -63,4 +63,9 @@ class AuthCubit extends Cubit { ); } } + + Future requestLogout() async { + await _supabase.auth.signOut(); + emit(state.copyWith(status: AuthStatus.initial)); + } } diff --git a/lib/features/auth/ui/auth_screen.dart b/lib/features/auth/ui/auth_screen.dart index 0f3486e..00543d9 100644 --- a/lib/features/auth/ui/auth_screen.dart +++ b/lib/features/auth/ui/auth_screen.dart @@ -100,7 +100,7 @@ class _AuthScreenState extends State { label: 'Email Aziendale', icon: Icons.email_outlined, controller: _emailController, - // TODO: Aggiungi nel tuo FluxTextField la gestione del keyboardType se non c'è già! + keyboardType: TextInputType.emailAddress, ), const SizedBox(height: 20), FluxTextField( @@ -108,7 +108,8 @@ class _AuthScreenState extends State { icon: Icons.lock_outline, isPassword: true, // Magia del FluxTextField! controller: _passwordController, - // onSubmitted: (_) => _submit(), // Se lo supporti nel tuo widget custom + onSubmitted: (_) => + _submit(), // Se lo supporti nel tuo widget custom ), const SizedBox(height: 40), diff --git a/lib/features/home/ui/home_screen.dart b/lib/features/home/ui/home_screen.dart index 59dc14e..bf11ead 100644 --- a/lib/features/home/ui/home_screen.dart +++ b/lib/features/home/ui/home_screen.dart @@ -246,9 +246,7 @@ class _HomeScreenState extends State { ), onPressed: () { Navigator.pop(dialogContext); // Chiude la Dialog - /* context.read().add( - LogoutRequested(), - ); // Esegue il logout */ + context.read().requestLogout(); // Esegue il logout }, child: const Text("Esci"), ), diff --git a/lib/features/onboarding/blocs/onboarding_cubit.dart b/lib/features/onboarding/blocs/onboarding_cubit.dart index d23f876..5c7186c 100644 --- a/lib/features/onboarding/blocs/onboarding_cubit.dart +++ b/lib/features/onboarding/blocs/onboarding_cubit.dart @@ -13,7 +13,11 @@ class OnboardingCubit extends Cubit { final SessionCubit _sessionCubit; OnboardingCubit(this._sessionCubit, this._repository) - : super(OnboardingState(step: _sessionCubit.state.onboardingStep)); + : super(OnboardingState( + step: _sessionCubit.state.onboardingStep, + companyId: _sessionCubit.state.company?.id, + storeId: _sessionCubit.state.currentStore?.id, + )); // --- STEP 1: REGISTRAZIONE AZIENDA --- Future saveCompany(String companyName) async { @@ -49,12 +53,14 @@ class OnboardingCubit extends Cubit { // --- STEP 2: REGISTRAZIONE PRIMO NEGOZIO --- Future saveStore(StoreModel store) async { if (state.companyId == null) return; + if (state.companyId == '') return; emit(state.copyWith(isLoading: true)); try { // Iniettiamo forzatamente il companyId ottenuto dallo step precedente final storeToSave = store.copyWith(companyId: state.companyId); final savedStore = await _repository.createStore(storeToSave); + _sessionCubit.changeStore(savedStore); emit( state.copyWith( @@ -72,7 +78,8 @@ class OnboardingCubit extends Cubit { // --- STEP 3: REGISTRAZIONE PROFILO STAFF (PAZIENTE ZERO) --- Future saveStaff(StaffMemberModel staff) async { - if (state.companyId == null || state.storeId == null) return; + if (state.companyId == null) return; + if (state.companyId == '') return; emit(state.copyWith(isLoading: true)); try { diff --git a/lib/features/onboarding/ui/company_onboarding_form.dart b/lib/features/onboarding/ui/company_onboarding_form.dart index 9225da6..6a06b08 100644 --- a/lib/features/onboarding/ui/company_onboarding_form.dart +++ b/lib/features/onboarding/ui/company_onboarding_form.dart @@ -50,6 +50,10 @@ class _CompanyOnboardingFormState extends State { label: 'Ragione Sociale / Nome Azienda', controller: _nameCtrl, validator: notEmptyValidator, + keyboardType: TextInputType.name, + textCapitalization: TextCapitalization.words, + autocorrect: false, + onSubmitted: (_) => _submit(), ), const Spacer(), @@ -60,13 +64,7 @@ class _CompanyOnboardingFormState extends State { borderRadius: BorderRadius.circular(12), ), ), - onPressed: () { - if (_formKey.currentState!.validate()) { - context.read().saveCompany( - _nameCtrl.text.trim(), - ); - } - }, + onPressed: () => _submit(), child: const Text( "Salva e Prosegui", style: TextStyle(fontSize: 16), @@ -78,4 +76,10 @@ class _CompanyOnboardingFormState extends State { ), ); } + + void _submit() { + if (_formKey.currentState!.validate()) { + context.read().saveCompany(_nameCtrl.text.trim()); + } + } } diff --git a/lib/features/onboarding/ui/onboarding_screen.dart b/lib/features/onboarding/ui/onboarding_screen.dart index 46a8906..926c894 100644 --- a/lib/features/onboarding/ui/onboarding_screen.dart +++ b/lib/features/onboarding/ui/onboarding_screen.dart @@ -1,18 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; -import 'package:flux/core/data/core_repository.dart'; -import 'package:flux/core/utils/validators.dart'; -import 'package:flux/features/master_data/store/models/store_model.dart'; -import 'package:flux/features/master_data/staff/models/staff_member_model.dart'; import 'package:flux/features/onboarding/blocs/onboarding_cubit.dart'; - -// Sostituisci con il percorso corretto della tua FluxTextField -import 'package:flux/core/widgets/flux_text_field.dart'; import 'package:flux/features/onboarding/blocs/onboarding_state.dart'; import 'package:flux/features/onboarding/ui/company_onboarding_form.dart'; +import 'package:flux/features/onboarding/ui/staff_onboarding_form.dart'; import 'package:flux/features/onboarding/ui/store_onboarding_form.dart'; -import 'package:get_it/get_it.dart'; class OnboardingScreen extends StatefulWidget { const OnboardingScreen({super.key}); @@ -24,15 +17,6 @@ class OnboardingScreen extends StatefulWidget { class _OnboardingScreenState extends State { late PageController _pageController; - // --- CHIAVI DEI FORM (Per la validazione indipendente di ogni step) --- - - final _staffFormKey = GlobalKey(); - - // --- CONTROLLERS: STEP 3 (Staff) --- - final _staffFirstNameCtrl = TextEditingController(); - final _staffLastNameCtrl = TextEditingController(); - final _staffJobTitleCtrl = TextEditingController(); - @override void initState() { super.initState(); @@ -44,9 +28,6 @@ class _OnboardingScreenState extends State { @override void dispose() { _pageController.dispose(); - _staffFirstNameCtrl.dispose(); - _staffLastNameCtrl.dispose(); - _staffJobTitleCtrl.dispose(); super.dispose(); } @@ -112,9 +93,9 @@ class _OnboardingScreenState extends State { physics: const NeverScrollableScrollPhysics(), // Vietato lo swipe manuale! children: [ - CompanyOnboardingForm(state: state), // Step 1: Company + CompanyOnboardingForm(state: state), StoreOnboardingForm(state: state), - _buildStaffForm(context, state), + StaffOnboardingForm(), ], ), @@ -131,73 +112,4 @@ class _OnboardingScreenState extends State { }, ); } - - Widget _buildStaffForm(BuildContext context, OnboardingState state) { - return Padding( - padding: const EdgeInsets.all(32.0), - child: Form( - key: _staffFormKey, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const Text( - "Il tuo Profilo 👤", - style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), - const Text( - "Ultimo step! Crea il tuo profilo operativo per iniziare a usare FLUX.", - style: TextStyle(fontSize: 16, color: Colors.grey), - ), - const SizedBox(height: 48), - - FluxTextField( - label: 'Nome', - controller: _staffFirstNameCtrl, - validator: notEmptyValidator, - ), - const SizedBox(height: 16), - FluxTextField( - label: 'Cognome', - controller: _staffLastNameCtrl, - validator: notEmptyValidator, - ), - const SizedBox(height: 16), - FluxTextField( - label: 'Etichetta Ruolo (es. Titolare, Manager)', - controller: _staffJobTitleCtrl, - // Il jobTitle può anche essere opzionale, decidi tu! - ), - - const Spacer(), - ElevatedButton( - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 16), - backgroundColor: Colors.black, // O il tuo context.accent - foregroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - onPressed: () { - if (_staffFormKey.currentState!.validate()) { - final newStaff = StaffMemberModel.empty().copyWith( - name: _staffFirstNameCtrl.text.trim(), - jobTitle: _staffJobTitleCtrl.text.trim(), - ); - context.read().saveStaff(newStaff); - } - }, - child: const Text( - "Entra in FLUX", - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), - ), - ), - const SizedBox(height: 16), - ], - ), - ), - ); - } } diff --git a/lib/features/onboarding/ui/staff_onboarding_form.dart b/lib/features/onboarding/ui/staff_onboarding_form.dart new file mode 100644 index 0000000..a280058 --- /dev/null +++ b/lib/features/onboarding/ui/staff_onboarding_form.dart @@ -0,0 +1,105 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flux/core/utils/validators.dart'; +import 'package:flux/core/widgets/flux_text_field.dart'; +import 'package:flux/features/master_data/staff/models/staff_member_model.dart'; +import 'package:flux/features/onboarding/blocs/onboarding_cubit.dart'; + +class StaffOnboardingForm extends StatefulWidget { + const StaffOnboardingForm({super.key}); + + @override + State createState() => _StaffOnboardingFormState(); +} + +class _StaffOnboardingFormState extends State { + final _formKey = GlobalKey(); + final _nameCtrl = TextEditingController(); + final _emailCtrl = TextEditingController(); + final _jobTitleCtrl = TextEditingController(); + + @override + void dispose() { + _nameCtrl.dispose(); + _emailCtrl.dispose(); + _jobTitleCtrl.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(32), + child: Form( + key: _formKey, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Text( + "Il tuo Profilo 👤", + style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + const Text( + "Ultimo step! Crea il tuo profilo operativo per iniziare a usare FLUX.", + style: TextStyle(fontSize: 16, color: Colors.grey), + ), + const SizedBox(height: 48), + FluxTextField( + label: 'Nome', + keyboardType: TextInputType.name, + controller: _nameCtrl, + validator: notEmptyValidator, + textCapitalization: TextCapitalization.words, + autocorrect: false, + ), + const SizedBox(height: 16), + FluxTextField( + label: 'Email', + keyboardType: TextInputType.emailAddress, + controller: _emailCtrl, + textCapitalization: TextCapitalization.none, + ), + const SizedBox(height: 16), + FluxTextField( + label: 'Etichetta Ruolo (es. Titolare, Manager)', + controller: _jobTitleCtrl, + keyboardType: TextInputType.text, + textCapitalization: TextCapitalization.words, + onSubmitted: (_) => _submit(), + ), + const Spacer(), + ElevatedButton( + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + backgroundColor: Colors.black, // O il tuo context.accent + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + onPressed: () => _submit(), + child: const Text( + "Entra in FLUX", + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + ), + const SizedBox(height: 16), + ], + ), + ), + ); + } + + void _submit() { + if (_formKey.currentState!.validate()) { + final newStaff = StaffMemberModel.empty().copyWith( + name: _nameCtrl.text.trim(), + email: _emailCtrl.text.trim(), + jobTitle: _jobTitleCtrl.text.trim(), + ); + context.read().saveStaff(newStaff); + } + } +} diff --git a/lib/features/onboarding/ui/store_onboarding_form.dart b/lib/features/onboarding/ui/store_onboarding_form.dart index fa8039a..0f26732 100644 --- a/lib/features/onboarding/ui/store_onboarding_form.dart +++ b/lib/features/onboarding/ui/store_onboarding_form.dart @@ -56,6 +56,7 @@ class _StoreOnboardingFormState extends State { FluxTextField( controller: _nameCtrl, label: "Nome del Negozio", + keyboardType: TextInputType.name, validator: (value) => value == null || value.isEmpty ? "Obbligatorio" : null, ), @@ -63,6 +64,7 @@ class _StoreOnboardingFormState extends State { FluxTextField( controller: _addressCtrl, + keyboardType: TextInputType.streetAddress, label: "Indirizzo", validator: (value) => value == null || value.isEmpty ? "Obbligatorio" : null, @@ -115,20 +117,7 @@ class _StoreOnboardingFormState extends State { borderRadius: BorderRadius.circular(12), ), ), - onPressed: () { - if (_formKey.currentState!.validate()) { - // MIRACOLO DELLA FACTORY EMPTY! - final newStore = StoreModel.empty().copyWith( - nome: _nameCtrl.text.trim(), - indirizzo: _addressCtrl.text.trim(), - comune: _cityCtrl.text.trim(), - cap: _zipCodeCtrl.text.trim(), - // Formattiamo in maiuscolo qui, al momento del salvataggio! - provincia: _provinceCtrl.text.trim().toUpperCase(), - ); - context.read().saveStore(newStore); - } - }, + onPressed: () => _submit(), child: const Text( "Salva Negozio", style: TextStyle(fontSize: 16), @@ -141,6 +130,21 @@ class _StoreOnboardingFormState extends State { ); } + void _submit() { + if (_formKey.currentState!.validate()) { + // MIRACOLO DELLA FACTORY EMPTY! + final newStore = StoreModel.empty().copyWith( + nome: _nameCtrl.text.trim(), + indirizzo: _addressCtrl.text.trim(), + comune: _cityCtrl.text.trim(), + cap: _zipCodeCtrl.text.trim(), + // Formattiamo in maiuscolo qui, al momento del salvataggio! + provincia: _provinceCtrl.text.trim().toUpperCase(), + ); + context.read().saveStore(newStore); + } + } + // --- WIDGET ESTRATTI PER PULIZIA --- Widget _buildCityField() { @@ -169,6 +173,7 @@ class _StoreOnboardingFormState extends State { // Rende la tastiera del telefono automaticamente maiuscola textCapitalization: TextCapitalization.characters, inputFormatters: [LengthLimitingTextInputFormatter(2)], + onSubmitted: (_) => _submit(), ); } } diff --git a/lib/main.dart b/lib/main.dart index 7d00799..4698234 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -66,6 +66,7 @@ Future setupLocator() async { url: dotenv.env['SUPABASE_URL'] ?? '', anonKey: dotenv.env['SUPABASE_ANON_KEY'] ?? '', ); + //await Supabase.instance.client.auth.signOut(); getIt.registerSingleton(Supabase.instance.client); // Settings