From 09398a1b342bfe3a368978f548768f33dabad663 Mon Sep 17 00:00:00 2001 From: Mark M2 Macbook Date: Tue, 21 Apr 2026 11:26:42 +0200 Subject: [PATCH] pennellato git da rosso ad arancio e verde --- lib/core/blocs/session/session_cubit.dart | 2 +- lib/core/routes/app_router.dart | 8 +- lib/core/utils/validators.dart | 6 + .../company/models/company_model.dart | 16 ++ .../company/ui/create_company_screen.dart | 6 +- .../customers/blocs/customer_cubit.dart | 8 +- .../customers/data/customer_repository.dart | 2 +- .../customers/ui/customers_content.dart | 6 +- lib/features/home/ui/dashboard_content.dart | 4 +- lib/features/home/ui/home_screen.dart | 8 +- .../products/blocs/product_cubit.dart | 11 +- .../providers/blocs/provider_cubit.dart | 6 +- .../master_data/staff/blocs/staff_cubit.dart | 4 +- .../staff/models/staff_member_model.dart | 60 +++-- .../master_data/staff/ui/staff_screen.dart | 23 +- .../master_data/store/bloc/store_cubit.dart | 4 +- .../master_data/store/models/store_model.dart | 11 + .../store/ui/create_store_screen.dart | 14 +- .../master_data/store/ui/store_form.dart | 4 +- .../onboarding/blocs/onboarding_cubit.dart | 1 - .../ui/company_onboarding_form.dart | 83 +++++++ .../onboarding/ui/onboarding_screen.dart | 110 +-------- .../onboarding/ui/onboarding_view.dart | 211 ------------------ .../services/blocs/services_cubit.dart | 8 +- .../services/data/services_repository.dart | 2 +- .../entertainment_service_card.dart | 2 +- .../services/utils/service_actions.dart | 6 +- 27 files changed, 237 insertions(+), 389 deletions(-) create mode 100644 lib/core/utils/validators.dart create mode 100644 lib/features/onboarding/ui/company_onboarding_form.dart delete mode 100644 lib/features/onboarding/ui/onboarding_view.dart diff --git a/lib/core/blocs/session/session_cubit.dart b/lib/core/blocs/session/session_cubit.dart index f476c2c..d3d7d8f 100644 --- a/lib/core/blocs/session/session_cubit.dart +++ b/lib/core/blocs/session/session_cubit.dart @@ -51,7 +51,7 @@ class SessionCubit extends Cubit { } // 2. Controllo Negozi - final stores = await _repository.getStoresByCompanyId(company.id); + final stores = await _repository.getStoresByCompanyId(company.id!); if (stores.isEmpty) { return emit( state.copyWith( diff --git a/lib/core/routes/app_router.dart b/lib/core/routes/app_router.dart index 4dc2953..dd59f98 100644 --- a/lib/core/routes/app_router.dart +++ b/lib/core/routes/app_router.dart @@ -1,8 +1,11 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flux/features/auth/ui/auth_screen.dart'; import 'package:flux/features/customers/models/customer_model.dart'; import 'package:flux/features/customers/ui/customer_detail_screen.dart'; +import 'package:flux/features/home/ui/home_screen.dart'; import 'package:flux/features/master_data/products/ui/products_screen.dart'; +import 'package:flux/features/onboarding/ui/onboarding_screen.dart'; import 'package:flux/features/services/models/service_model.dart'; import 'package:flux/features/services/ui/service_form_screen/service_form_screen.dart'; import 'package:go_router/go_router.dart'; @@ -58,7 +61,8 @@ class AppRouter { routes: [ GoRoute( path: '/login', - builder: (context, state) => const LoginScreen(), + //builder: (context, state) => const LoginScreen(), + builder: (context, state) => const AuthScreen(), ), GoRoute( path: '/onboarding', @@ -69,7 +73,7 @@ class AppRouter { ), GoRoute( path: '/', - builder: (context, state) => const DashboardScreen(), // La tua home + builder: (context, state) => const HomeScreen(), // La tua home ), GoRoute( path: '/customer/:id', diff --git a/lib/core/utils/validators.dart b/lib/core/utils/validators.dart new file mode 100644 index 0000000..c689a60 --- /dev/null +++ b/lib/core/utils/validators.dart @@ -0,0 +1,6 @@ +String? notEmptyValidator(String? value) { + if (value == null || value.trim().isEmpty) { + return 'Campo obbligatorio'; + } + return null; +} diff --git a/lib/features/company/models/company_model.dart b/lib/features/company/models/company_model.dart index 65406af..7cc8013 100644 --- a/lib/features/company/models/company_model.dart +++ b/lib/features/company/models/company_model.dart @@ -132,6 +132,22 @@ class CompanyModel extends Equatable { ); } + factory CompanyModel.empty() { + return const CompanyModel( + id: null, + createdAt: null, + userId: '', + ragioneSociale: '', + indirizzo: '', + cap: '', + citta: '', + provincia: '', + partitaIva: '', + codiceFiscale: '', + codiceUnivoco: '', + ); + } + factory CompanyModel.fromMap(Map map) { return CompanyModel( id: map['id'] as String?, diff --git a/lib/features/company/ui/create_company_screen.dart b/lib/features/company/ui/create_company_screen.dart index fa2ecf6..0127f0f 100644 --- a/lib/features/company/ui/create_company_screen.dart +++ b/lib/features/company/ui/create_company_screen.dart @@ -46,7 +46,7 @@ class _CreateCompanyScreenState extends State { void _onSave() { if (_formKey.currentState!.validate()) { // Recuperiamo l'ID utente attuale da Supabase o dal SessionBloc - final userId = context.read().state.userId!; + final userId = context.read().state.user!.id; final company = CompanyModel( userId: userId, @@ -77,7 +77,7 @@ class _CreateCompanyScreenState extends State { onPressed: () { // Qui chiami il tuo Bloc dell'autenticazione per fare logout // Esempio se hai un AuthBloc o SessionBloc: - context.read().add(LogoutRequested()); + //context.read().add(LogoutRequested()); // Se vuoi solo tornare brutalmente alla login per testare il logo: // Navigator.of(context).pushReplacementNamed('/login'); @@ -92,7 +92,7 @@ class _CreateCompanyScreenState extends State { //GetIt.I.get().setCurrentCompany(state.company); // 2. Notifichiamo il SessionBloc per cambiare pagina - context.read().add(AppStarted()); + //context.read().add(AppStarted()); } if (state.status == CompanyStatus.failure) { diff --git a/lib/features/customers/blocs/customer_cubit.dart b/lib/features/customers/blocs/customer_cubit.dart index 248913c..64f4882 100644 --- a/lib/features/customers/blocs/customer_cubit.dart +++ b/lib/features/customers/blocs/customer_cubit.dart @@ -10,7 +10,7 @@ part 'customer_state.dart'; class CustomerCubit extends Cubit { final CustomerRepository _repository = GetIt.I(); - final SessionBloc _sessionBloc = GetIt.I(); + final SessionCubit _sessionCubit = GetIt.I(); // Variabile per gestire il debounce della ricerca Timer? _searchDebounce; @@ -22,7 +22,7 @@ class CustomerCubit extends Cubit { emit(state.copyWith(status: CustomerStatus.loading)); try { final customers = await _repository.getCustomers( - _sessionBloc.state.company!.id, + _sessionCubit.state.company!.id!, ); emit( state.copyWith(status: CustomerStatus.success, customers: customers), @@ -111,7 +111,7 @@ class CustomerCubit extends Cubit { // Nessun "loading" state qui, per evitare sfarfallii visivi mentre si scrive try { final results = await _repository.searchCustomers( - _sessionBloc.state.company!.id, + _sessionCubit.state.company!.id!, query, ); emit( @@ -137,7 +137,7 @@ class CustomerCubit extends Cubit { nome: name, telefono: phone ?? '', email: email ?? '', - companyId: _sessionBloc.state.company!.id, + companyId: _sessionCubit.state.company!.id!, note: '', ); diff --git a/lib/features/customers/data/customer_repository.dart b/lib/features/customers/data/customer_repository.dart index 463226e..6b3f3c2 100644 --- a/lib/features/customers/data/customer_repository.dart +++ b/lib/features/customers/data/customer_repository.dart @@ -8,7 +8,7 @@ import '../models/customer_model.dart'; class CustomerRepository { final SupabaseClient _supabase = GetIt.I(); - final String companyId = GetIt.I.get().state.company!.id; + final String companyId = GetIt.I.get().state.company!.id!; // Crea un nuovo cliente Future saveCustomer(CustomerModel customer) async { diff --git a/lib/features/customers/ui/customers_content.dart b/lib/features/customers/ui/customers_content.dart index 6f46905..d9eb0b1 100644 --- a/lib/features/customers/ui/customers_content.dart +++ b/lib/features/customers/ui/customers_content.dart @@ -24,14 +24,14 @@ class _CustomersContentState extends State { } void _loadInitialCustomers() { - final companyId = context.read().state.company?.id; + final companyId = context.read().state.company?.id; if (companyId != null) { context.read().loadCustomers(); } } void _onSearch(String query) { - final companyId = context.read().state.company?.id; + final companyId = context.read().state.company?.id; if (companyId != null) { context.read().searchCustomers(query); } @@ -48,7 +48,7 @@ class _CustomersContentState extends State { child: CustomerForm( customer: customer, onSave: (customerFromForm) { - final session = context.read().state; + final session = context.read().state; final companyId = session.company?.id; if (companyId == null) return; diff --git a/lib/features/home/ui/dashboard_content.dart b/lib/features/home/ui/dashboard_content.dart index 3c82a27..d32b204 100644 --- a/lib/features/home/ui/dashboard_content.dart +++ b/lib/features/home/ui/dashboard_content.dart @@ -16,9 +16,9 @@ class DashboardContent extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocBuilder( + return BlocBuilder( builder: (context, state) { - final store = state.selectedStore; + final store = state.currentStore; final company = state.company; return Scaffold( diff --git a/lib/features/home/ui/home_screen.dart b/lib/features/home/ui/home_screen.dart index d0f3c39..59dc14e 100644 --- a/lib/features/home/ui/home_screen.dart +++ b/lib/features/home/ui/home_screen.dart @@ -30,7 +30,7 @@ class _HomeScreenState extends State { @override Widget build(BuildContext context) { - return BlocBuilder( + return BlocBuilder( builder: (context, state) { return LayoutBuilder( builder: (context, constraints) { @@ -203,7 +203,7 @@ class _HomeScreenState extends State { ), const SizedBox(width: 12), Text( - GetIt.I.get().state.company?.ragioneSociale ?? + GetIt.I.get().state.company?.ragioneSociale ?? "Utente", style: TextStyle( fontWeight: FontWeight.bold, @@ -246,9 +246,9 @@ class _HomeScreenState extends State { ), onPressed: () { Navigator.pop(dialogContext); // Chiude la Dialog - context.read().add( + /* context.read().add( LogoutRequested(), - ); // Esegue il logout + ); // Esegue il logout */ }, child: const Text("Esci"), ), diff --git a/lib/features/master_data/products/blocs/product_cubit.dart b/lib/features/master_data/products/blocs/product_cubit.dart index 973dd35..b35a6bc 100644 --- a/lib/features/master_data/products/blocs/product_cubit.dart +++ b/lib/features/master_data/products/blocs/product_cubit.dart @@ -11,7 +11,7 @@ part 'product_state.dart'; class ProductCubit extends Cubit { final ProductRepository _repository = GetIt.I(); - final SessionBloc _sessionBloc = GetIt.I(); + final SessionCubit _sessionCubit = GetIt.I(); ProductCubit() : super(const ProductState()); @@ -20,7 +20,7 @@ class ProductCubit extends Cubit { emit(state.copyWith(status: ProductStatus.loading)); try { final brands = await _repository.getBrands( - _sessionBloc.state.company!.id, + _sessionCubit.state.company!.id!, ); emit(state.copyWith(status: ProductStatus.success, brands: brands)); } catch (e) { @@ -54,7 +54,7 @@ class ProductCubit extends Cubit { final brand = BrandModel( id: id, name: name, - companyId: _sessionBloc.state.company!.id, + companyId: _sessionCubit.state.company!.id!, ); final newBrand = await _repository.upsertBrand(brand); await loadBrands(); // Ricarichiamo la lista aggiornata @@ -137,7 +137,10 @@ class ProductCubit extends Cubit { // 1. Cerchiamo o creiamo il Brand // (Usa una funzione upsert o una ricerca rapida nel repository) brand ??= await _repository.upsertBrand( - BrandModel(name: brandName, companyId: _sessionBloc.state.company!.id), + BrandModel( + name: brandName, + companyId: _sessionCubit.state.company!.id!, + ), ); // 2. Creiamo il Modello legato al Brand diff --git a/lib/features/master_data/providers/blocs/provider_cubit.dart b/lib/features/master_data/providers/blocs/provider_cubit.dart index 857c932..8ca498c 100644 --- a/lib/features/master_data/providers/blocs/provider_cubit.dart +++ b/lib/features/master_data/providers/blocs/provider_cubit.dart @@ -52,7 +52,7 @@ class ProvidersState extends Equatable { class ProvidersCubit extends Cubit { final ProviderRepository _repository = GetIt.I(); - final SessionBloc _sessionBloc = GetIt.I(); + final SessionCubit _sessionCubit = GetIt.I(); ProvidersCubit() : super(const ProvidersState()); @@ -61,7 +61,7 @@ class ProvidersCubit extends Cubit { emit(state.copyWith(isLoading: true)); try { final all = await _repository.fetchAllCompanyProviders( - _sessionBloc.state.company!.id, + _sessionCubit.state.company!.id!, ); List associated = []; @@ -135,7 +135,7 @@ class ProvidersCubit extends Cubit { ) async { emit(state.copyWith(isLoading: true)); // Assicuriamoci di settare la companyId prima di salvare - provider = provider.copyWith(companyId: _sessionBloc.state.company!.id); + provider = provider.copyWith(companyId: _sessionCubit.state.company!.id); try { // 1. Salviamo l'anagrafica (upsert) // Se è un nuovo provider, l'ID potrebbe essere generato qui dal DB diff --git a/lib/features/master_data/staff/blocs/staff_cubit.dart b/lib/features/master_data/staff/blocs/staff_cubit.dart index 4c3fb60..9c37a93 100644 --- a/lib/features/master_data/staff/blocs/staff_cubit.dart +++ b/lib/features/master_data/staff/blocs/staff_cubit.dart @@ -10,7 +10,7 @@ part 'staff_state.dart'; class StaffCubit extends Cubit { final StaffRepository _repository = GetIt.I.get(); - final SessionBloc _sessionBloc = GetIt.I(); + final SessionCubit _sessionCubit = GetIt.I(); StaffCubit() : super(const StaffState()); @@ -19,7 +19,7 @@ class StaffCubit extends Cubit { emit(state.copyWith(isLoading: true, error: null)); try { final staff = await _repository.getStaffMembers( - _sessionBloc.state.company!.id, + _sessionCubit.state.company!.id!, ); final Map> storesByStaff = {}; for (StaffMemberModel member in staff) { diff --git a/lib/features/master_data/staff/models/staff_member_model.dart b/lib/features/master_data/staff/models/staff_member_model.dart index 3f8a1c5..1368716 100644 --- a/lib/features/master_data/staff/models/staff_member_model.dart +++ b/lib/features/master_data/staff/models/staff_member_model.dart @@ -18,44 +18,61 @@ enum SystemRole { class StaffMemberModel extends Equatable { final String? id; final String companyId; - final String storeId; final String userId; final String name; - final String surname; - final String? - jobTitle; // Testo libero! Il cliente ci scrive quello che vuole. - final SystemRole systemRole; // ENUM! Il sistema non si frega. + final String? email; + final String? phoneNumber; + final String? jobTitle; + final SystemRole systemRole; + final bool isActive; const StaffMemberModel({ this.id, required this.companyId, - required this.storeId, required this.userId, required this.name, - required this.surname, + this.email, + this.phoneNumber, this.jobTitle, - this.systemRole = SystemRole.user, // Sicurezza di default + this.systemRole = SystemRole.user, + this.isActive = true, }); StaffMemberModel copyWith({ String? id, String? companyId, - String? storeId, String? userId, String? name, String? surname, + String? email, + String? phoneNumber, String? jobTitle, SystemRole? systemRole, + bool? isActive, }) { return StaffMemberModel( id: id ?? this.id, companyId: companyId ?? this.companyId, - storeId: storeId ?? this.storeId, userId: userId ?? this.userId, name: name ?? this.name, - surname: surname ?? this.surname, + email: email ?? this.email, + phoneNumber: phoneNumber ?? this.phoneNumber, jobTitle: jobTitle ?? this.jobTitle, systemRole: systemRole ?? this.systemRole, + isActive: isActive ?? this.isActive, + ); + } + + factory StaffMemberModel.empty() { + return const StaffMemberModel( + companyId: '', + userId: '', + name: '', + email: '', + phoneNumber: '', + jobTitle: '', + systemRole: SystemRole.user, + isActive: true, ); } @@ -63,14 +80,13 @@ class StaffMemberModel extends Equatable { return StaffMemberModel( id: map['id'] as String?, companyId: map['company_id'] ?? '', - storeId: map['store_id'] ?? '', userId: map['user_id'] ?? '', name: map['name'] ?? '', - surname: map['surname'] ?? '', - jobTitle: map['job_title'] as String?, // Semplice stringa - systemRole: SystemRole.fromString( - map['system_role'], - ), // Lettura tipizzata + email: map['email'] as String?, + phoneNumber: map['phone_number'] as String?, + jobTitle: map['job_title'] as String?, + systemRole: SystemRole.fromString(map['system_role']), + isActive: map['is_active'] ?? true, ); } @@ -78,12 +94,13 @@ class StaffMemberModel extends Equatable { return { if (id != null) 'id': id, 'company_id': companyId, - 'store_id': storeId, 'user_id': userId, 'name': name, - 'surname': surname, + if (email != null) 'email': email, + if (phoneNumber != null) 'phone_number': phoneNumber, if (jobTitle != null) 'job_title': jobTitle, 'system_role': systemRole.name, // Trasforma SystemRole.admin in 'admin' + 'is_active': isActive, }; } @@ -91,11 +108,12 @@ class StaffMemberModel extends Equatable { List get props => [ id, companyId, - storeId, userId, name, - surname, + email, + phoneNumber, jobTitle, systemRole, + isActive, ]; } diff --git a/lib/features/master_data/staff/ui/staff_screen.dart b/lib/features/master_data/staff/ui/staff_screen.dart index b1dd3bb..bb70842 100644 --- a/lib/features/master_data/staff/ui/staff_screen.dart +++ b/lib/features/master_data/staff/ui/staff_screen.dart @@ -135,8 +135,13 @@ class _StaffScreenState extends State { subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (member.email.isNotEmpty) Text(member.email), - Text(member.phone.isNotEmpty ? member.phone : "Nessun telefono"), + if (member.email != null && member.email!.isNotEmpty) + Text(member.email!), + Text( + member.phoneNumber != null && member.phoneNumber!.isNotEmpty + ? member.phoneNumber! + : "Nessun telefono", + ), ], ), trailing: const Icon(Icons.edit_note), @@ -148,7 +153,7 @@ class _StaffScreenState extends State { void _openStaffForm(BuildContext context, {StaffMemberModel? member}) { final nameController = TextEditingController(text: member?.name); final emailController = TextEditingController(text: member?.email); - final phoneController = TextEditingController(text: member?.phone); + final phoneController = TextEditingController(text: member?.phoneNumber); // 1. Inizializziamo la lista temporanea attingendo dallo stato del Cubit // Usiamo storesByStaff (la mappa che indicizza i negozi per ogni ID dipendente) @@ -264,16 +269,16 @@ class _StaffScreenState extends State { child: ElevatedButton( onPressed: () { final companyId = context - .read() + .read() .state .company! - .id; - - final updatedMember = StaffMemberModel( + .id!; + //TODO sistemare StaffScreen per il nuovo modello + /* final updatedMember = StaffMemberModel( id: member?.id, name: nameController.text, email: emailController.text, - phone: phoneController.text, + phoneNumber: phoneController.text, companyId: companyId, ); @@ -281,7 +286,7 @@ class _StaffScreenState extends State { context.read().saveStaffWithStores( member: updatedMember, selectedStoreIds: tempSelectedStores, - ); + ); */ Navigator.pop(context); }, diff --git a/lib/features/master_data/store/bloc/store_cubit.dart b/lib/features/master_data/store/bloc/store_cubit.dart index 9d83387..09d31d0 100644 --- a/lib/features/master_data/store/bloc/store_cubit.dart +++ b/lib/features/master_data/store/bloc/store_cubit.dart @@ -13,7 +13,7 @@ part 'store_state.dart'; class StoreCubit extends Cubit { final StoreRepository _repository = GetIt.I(); final StaffRepository _staffRepository = GetIt.I(); - final SessionBloc _sessionBloc = GetIt.I(); + final SessionCubit _sessionCubit = GetIt.I(); StoreCubit() : super(const StoreState(stores: [])); @@ -33,7 +33,7 @@ class StoreCubit extends Cubit { emit(state.copyWith(status: StoreStatus.loading)); try { final stores = await _repository.fetchAllCompanyStores( - _sessionBloc.state.company!.id, + _sessionCubit.state.company!.id!, ); final Map> staffByStore = {}; for (StoreModel store in stores) { diff --git a/lib/features/master_data/store/models/store_model.dart b/lib/features/master_data/store/models/store_model.dart index 5f65f86..f30e0c1 100644 --- a/lib/features/master_data/store/models/store_model.dart +++ b/lib/features/master_data/store/models/store_model.dart @@ -81,6 +81,17 @@ class StoreModel extends Equatable { ); } + factory StoreModel.empty() { + return const StoreModel( + nome: '', + companyId: '', + indirizzo: '', + cap: '', + comune: '', + provincia: '', + ); + } + factory StoreModel.fromMap(Map map) { final providersPivotList = map['associated_providers'] as List?; List providers = []; diff --git a/lib/features/master_data/store/ui/create_store_screen.dart b/lib/features/master_data/store/ui/create_store_screen.dart index 77fd41c..7ea5786 100644 --- a/lib/features/master_data/store/ui/create_store_screen.dart +++ b/lib/features/master_data/store/ui/create_store_screen.dart @@ -34,7 +34,7 @@ class _CreateStoreScreenState extends State { /// Funzione magica per copiare i dati dall'azienda salvata in GetIt void _useCompanyAddress() { - final company = context.read().state.company; + final company = context.read().state.company; if (company != null) { setState(() { _indirizzoController.text = company.indirizzo; @@ -58,7 +58,7 @@ class _CreateStoreScreenState extends State { void _onSave() { if (_formKey.currentState!.validate()) { - final company = context.read().state.company; + final company = context.read().state.company; if (company == null) { ScaffoldMessenger.of(context).showSnackBar( @@ -69,7 +69,7 @@ class _CreateStoreScreenState extends State { final store = StoreModel( nome: _nomeController.text.trim(), - companyId: company.id, + companyId: company.id!, indirizzo: _indirizzoController.text.trim(), cap: _capController.text.trim(), comune: _comuneController.text.trim(), @@ -84,10 +84,10 @@ class _CreateStoreScreenState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Il tuo primo Negozio')), - body: BlocConsumer( - listener: (context, state) { + body: BlocBuilder( + /* listener: (context, state) { if (state.status == StoreStatus.success) { - context.read().add(AppStarted()); + context.read().; } if (state.status == StoreStatus.failure) { ScaffoldMessenger.of(context).showSnackBar( @@ -96,7 +96,7 @@ class _CreateStoreScreenState extends State { ), ); } - }, + }, */ builder: (context, state) { return SafeArea( child: SingleChildScrollView( diff --git a/lib/features/master_data/store/ui/store_form.dart b/lib/features/master_data/store/ui/store_form.dart index d9be617..1795169 100644 --- a/lib/features/master_data/store/ui/store_form.dart +++ b/lib/features/master_data/store/ui/store_form.dart @@ -130,10 +130,10 @@ class _StoreFormState extends State { comune: comuneController.text, provincia: provinciaController.text, companyId: context - .read() + .read() .state .company! - .id, // Recuperiamo la companyId + .id!, // Recuperiamo la companyId isActive: widget.store?.isActive ?? true, isPaid: widget.store?.isPaid ?? false, paymentExpiration: widget.store?.paymentExpiration, diff --git a/lib/features/onboarding/blocs/onboarding_cubit.dart b/lib/features/onboarding/blocs/onboarding_cubit.dart index 82dbea0..ef430f1 100644 --- a/lib/features/onboarding/blocs/onboarding_cubit.dart +++ b/lib/features/onboarding/blocs/onboarding_cubit.dart @@ -70,7 +70,6 @@ class OnboardingCubit extends Cubit { // PARANOIA MODE: Forziamo i legami e il ruolo di sistema 'admin' final staffToSave = staff.copyWith( companyId: state.companyId!, - storeId: state.storeId!, userId: _sessionCubit.state.user!.id, // Dall'utente loggato in Supabase systemRole: SystemRole.admin, // Blindato! ); diff --git a/lib/features/onboarding/ui/company_onboarding_form.dart b/lib/features/onboarding/ui/company_onboarding_form.dart new file mode 100644 index 0000000..1ec1484 --- /dev/null +++ b/lib/features/onboarding/ui/company_onboarding_form.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flux/core/utils/validators.dart'; +import 'package:flux/core/widgets/flux_text_field.dart'; +import 'package:flux/features/company/models/company_model.dart'; +import 'package:flux/features/onboarding/blocs/onboarding_cubit.dart'; +import 'package:flux/features/onboarding/blocs/onboarding_state.dart'; + +class CompanyOnboardingForm extends StatefulWidget { + final OnboardingState state; + const CompanyOnboardingForm({super.key, required this.state}); + + @override + State createState() => _CompanyOnboardingFormState(); +} + +class _CompanyOnboardingFormState extends State { + final _formKey = GlobalKey(); + final _nameCtrl = TextEditingController(); + + @override + void dispose() { + _nameCtrl.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(32), + child: Form( + key: _formKey, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Text( + "Iniziamo! 🏢", + style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + const Text( + "Inserisci i dati della tua attività per configurare il tuo ambiente FLUX.", + style: TextStyle(fontSize: 16, color: Colors.grey), + ), + const SizedBox(height: 48), + + FluxTextField( + label: 'Ragione Sociale / Nome Azienda', + controller: _nameCtrl, + validator: notEmptyValidator, + ), + const SizedBox(height: 16), + + const Spacer(), + ElevatedButton( + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + onPressed: () { + if (_formKey.currentState!.validate()) { + final newCompany = CompanyModel.empty().copyWith( + ragioneSociale: _nameCtrl.text.trim(), + ); + context.read().saveCompany(newCompany); + } + }, + child: const Text( + "Salva e Prosegui", + style: TextStyle(fontSize: 16), + ), + ), + const SizedBox(height: 16), + ], + ), + ), + ); + } +} diff --git a/lib/features/onboarding/ui/onboarding_screen.dart b/lib/features/onboarding/ui/onboarding_screen.dart index bc26faa..76822a2 100644 --- a/lib/features/onboarding/ui/onboarding_screen.dart +++ b/lib/features/onboarding/ui/onboarding_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; +import 'package:flux/core/utils/validators.dart'; import 'package:flux/features/company/models/company_model.dart'; import 'package:flux/features/master_data/store/models/store_model.dart'; import 'package:flux/features/master_data/staff/models/staff_member_model.dart'; @@ -9,6 +10,7 @@ import 'package:flux/features/onboarding/blocs/onboarding_cubit.dart'; // Sostituisci con il percorso corretto della tua FluxTextField import 'package:flux/core/widgets/flux_text_field.dart'; import 'package:flux/features/onboarding/blocs/onboarding_state.dart'; +import 'package:flux/features/onboarding/ui/company_onboarding_form.dart'; class OnboardingScreen extends StatefulWidget { const OnboardingScreen({super.key}); @@ -21,14 +23,10 @@ class _OnboardingScreenState extends State { late PageController _pageController; // --- CHIAVI DEI FORM (Per la validazione indipendente di ogni step) --- - final _companyFormKey = GlobalKey(); + final _storeFormKey = GlobalKey(); final _staffFormKey = GlobalKey(); - // --- CONTROLLERS: STEP 1 (Company) --- - final _companyNameCtrl = TextEditingController(); - final _companyVatCtrl = TextEditingController(); - // --- CONTROLLERS: STEP 2 (Store) --- final _storeNameCtrl = TextEditingController(); final _storeAddressCtrl = TextEditingController(); @@ -49,8 +47,6 @@ class _OnboardingScreenState extends State { @override void dispose() { _pageController.dispose(); - _companyNameCtrl.dispose(); - _companyVatCtrl.dispose(); _storeNameCtrl.dispose(); _storeAddressCtrl.dispose(); _staffFirstNameCtrl.dispose(); @@ -72,14 +68,6 @@ class _OnboardingScreenState extends State { } } - // Validatore generico riutilizzabile - String? _requireValidator(String? value) { - if (value == null || value.trim().isEmpty) { - return 'Campo obbligatorio'; - } - return null; - } - @override Widget build(BuildContext context) { return BlocConsumer( @@ -129,7 +117,7 @@ class _OnboardingScreenState extends State { physics: const NeverScrollableScrollPhysics(), // Vietato lo swipe manuale! children: [ - _buildCompanyForm(context, state), + CompanyOnboardingForm(state: state), // Step 1: Company _buildStoreForm(context, state), _buildStaffForm(context, state), ], @@ -149,73 +137,6 @@ class _OnboardingScreenState extends State { ); } - // ========================================================================= - // SCHERMATE DEI SINGOLI STEP - // ========================================================================= - - Widget _buildCompanyForm(BuildContext context, OnboardingState state) { - return Padding( - padding: const EdgeInsets.all(32.0), - child: Form( - key: _companyFormKey, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const Text( - "Iniziamo! 🏢", - style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), - const Text( - "Inserisci i dati della tua attività per configurare il tuo ambiente FLUX.", - style: TextStyle(fontSize: 16, color: Colors.grey), - ), - const SizedBox(height: 48), - - FluxTextField( - label: 'Ragione Sociale / Nome Azienda', - controller: _companyNameCtrl, - validator: _requireValidator, - ), - const SizedBox(height: 16), - FluxTextField( - label: 'Partita IVA', - controller: _companyVatCtrl, - validator: _requireValidator, - ), - - const Spacer(), - ElevatedButton( - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - onPressed: () { - if (_companyFormKey.currentState!.validate()) { - // MOCK DI ESEMPIO: Sostituisci con il tuo vero CompanyModel - final newCompany = CompanyModel( - ownerId: '', // Questo lo gestirà o ignorerà il Cubit - name: _companyNameCtrl.text.trim(), - vatNumber: _companyVatCtrl.text.trim(), - ); - context.read().saveCompany(newCompany); - } - }, - child: const Text( - "Salva e Prosegui", - style: TextStyle(fontSize: 16), - ), - ), - const SizedBox(height: 16), - ], - ), - ), - ); - } - Widget _buildStoreForm(BuildContext context, OnboardingState state) { return Padding( padding: const EdgeInsets.all(32.0), @@ -239,13 +160,13 @@ class _OnboardingScreenState extends State { FluxTextField( label: 'Nome Negozio (es. Sede Centrale)', controller: _storeNameCtrl, - validator: _requireValidator, + validator: notEmptyValidator, ), const SizedBox(height: 16), FluxTextField( label: 'Indirizzo completo', controller: _storeAddressCtrl, - validator: _requireValidator, + validator: notEmptyValidator, ), const Spacer(), @@ -258,11 +179,9 @@ class _OnboardingScreenState extends State { ), onPressed: () { if (_storeFormKey.currentState!.validate()) { - final newStore = StoreModel( - companyId: '', // Iniettato dal Cubit - name: _storeNameCtrl.text.trim(), - address: _storeAddressCtrl.text.trim(), - isActive: true, + final newStore = StoreModel.empty().copyWith( + nome: _storeNameCtrl.text.trim(), + indirizzo: _storeAddressCtrl.text.trim(), ); context.read().saveStore(newStore); } @@ -302,13 +221,13 @@ class _OnboardingScreenState extends State { FluxTextField( label: 'Nome', controller: _staffFirstNameCtrl, - validator: _requireValidator, + validator: notEmptyValidator, ), const SizedBox(height: 16), FluxTextField( label: 'Cognome', controller: _staffLastNameCtrl, - validator: _requireValidator, + validator: notEmptyValidator, ), const SizedBox(height: 16), FluxTextField( @@ -329,14 +248,9 @@ class _OnboardingScreenState extends State { ), onPressed: () { if (_staffFormKey.currentState!.validate()) { - final newStaff = StaffMemberModel( - companyId: '', // Iniettato dal Cubit - storeId: '', // Iniettato dal Cubit - userId: '', // Iniettato dal Cubit + final newStaff = StaffMemberModel.empty().copyWith( name: _staffFirstNameCtrl.text.trim(), - surname: _staffLastNameCtrl.text.trim(), jobTitle: _staffJobTitleCtrl.text.trim(), - // systemRole non viene passato qui: la Paranoia Mode del Cubit forzerà "admin" ); context.read().saveStaff(newStaff); } diff --git a/lib/features/onboarding/ui/onboarding_view.dart b/lib/features/onboarding/ui/onboarding_view.dart deleted file mode 100644 index 94223c9..0000000 --- a/lib/features/onboarding/ui/onboarding_view.dart +++ /dev/null @@ -1,211 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/core/blocs/session/session_cubit.dart'; -import 'package:flux/core/widgets/flux_text_field.dart'; -import 'package:flux/features/onboarding/blocs/onboarding_cubit.dart'; -import 'package:flux/features/onboarding/blocs/onboarding_state.dart'; -// Importa i tuoi file (cubit, modelli, ecc.) - -class OnboardingScreen extends StatefulWidget { - const OnboardingScreen({super.key}); - - @override - State createState() => _OnboardingScreenState(); -} - -class _OnboardingScreenState extends State { - late PageController _pageController; - - @override - void initState() { - super.initState(); - // Inizializziamo il controller sulla pagina giusta. - // L'indice parte da 0. company=0, store=1, staff=2. - final initialStep = context.read().state.step; - _pageController = PageController(initialPage: _getPageIndex(initialStep)); - } - - @override - void dispose() { - _pageController.dispose(); - super.dispose(); - } - - int _getPageIndex(OnboardingStep step) { - switch (step) { - case OnboardingStep.company: - return 0; - case OnboardingStep.store: - return 1; - case OnboardingStep.staff: - return 2; - default: - return 0; - } - } - - @override - Widget build(BuildContext context) { - return BlocConsumer( - // Ascoltiamo solo quando cambia lo step per animare la pagina - listenWhen: (previous, current) => previous.step != current.step, - listener: (context, state) { - if (state.step == OnboardingStep.completed) { - // Il SessionCubit prenderà il controllo e farà il redirect, - // qui potremmo mostrare un bel toast di successo. - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text("Configurazione completata! 🚀")), - ); - return; - } - - final targetPage = _getPageIndex(state.step); - _pageController.animateToPage( - targetPage, - duration: const Duration(milliseconds: 500), - curve: Curves.easeInOut, - ); - }, - builder: (context, state) { - return Scaffold( - body: SafeArea( - child: Stack( - children: [ - // Il PageView disabilita lo scorrimento manuale con NeverScrollableScrollPhysics - PageView( - controller: _pageController, - physics: const NeverScrollableScrollPhysics(), - children: [ - _buildCompanyForm(context, state), - _buildStoreForm(context, state), - _buildStaffForm( - context, - state, - ), // Qui c'è la magia paranoica - ], - ), - - // Overlay di caricamento universale - if (state.isLoading) - Container( - color: Colors.black.withValues(alpha: 0.5), - child: const Center(child: CircularProgressIndicator()), - ), - ], - ), - ), - ); - }, - ); - } - - // --- I METODI DEI FORM --- - // (Nella realtà li metterai in file separati o widget custom per pulizia) - - Widget _buildCompanyForm(BuildContext context, OnboardingState state) { - // Controller e chiavi del form... - return Padding( - padding: const EdgeInsets.all(24.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const Text( - "Step 1: La tua Azienda", - style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), - const Text("Inserisci i dati della tua attività."), - const SizedBox(height: 32), - - // Esempio usando la tua FluxTextField - FluxTextField(label: 'Ragione Sociale / Nome Azienda'), - const SizedBox(height: 16), - FluxTextField(label: 'Partita IVA'), - - const Spacer(), - ElevatedButton( - onPressed: () { - // 1. Valida il form - // 2. Crea il CompanyModel - // 3. Chiama il Cubit: - // context.read().saveCompany(newCompany); - }, - child: const Text("Salva e prosegui"), - ), - ], - ), - ); - } - - Widget _buildStoreForm(BuildContext context, OnboardingState state) { - return Padding( - padding: const EdgeInsets.all(24.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const Text( - "Step 2: Il primo Negozio", - style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), - const Text("Dove si trova il tuo punto vendita principale?"), - const SizedBox(height: 32), - - FluxTextField(label: 'Nome Negozio (es. Sede Centrale)'), - const SizedBox(height: 16), - FluxTextField(label: 'Indirizzo'), - - const Spacer(), - ElevatedButton( - onPressed: () { - // context.read().saveStore(newStore); - }, - child: const Text("Salva Negozio"), - ), - ], - ), - ); - } - - Widget _buildStaffForm(BuildContext context, OnboardingState state) { - // NOTA PARANOICA: Qui chiediamo jobTitle (Testo libero), - // ma NON diamo modo di scegliere il system_role! - return Padding( - padding: const EdgeInsets.all(24.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const Text( - "Step 3: Il tuo Profilo", - style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), - const Text("Crea il tuo profilo operativo per iniziare a lavorare."), - const SizedBox(height: 32), - - FluxTextField(label: 'Nome'), - const SizedBox(height: 16), - FluxTextField(label: 'Cognome'), - const SizedBox(height: 16), - - // TESTO LIBERO! Il cliente ci scrive quello che vuole ("CEO", "Boss", "Stagista") - FluxTextField(label: 'Ruolo / Etichetta (es. Titolare)'), - - const Spacer(), - ElevatedButton( - onPressed: () { - // Quando chiami il Cubit, passi i dati della UI. - // Ti ricordi? L'OnboardingCubit forzerà `system_role = SystemRole.admin` - // dietro le quinte! - // context.read().saveStaff(newStaff); - }, - child: const Text("Inizia a usare Flux!"), - ), - ], - ), - ); - } -} diff --git a/lib/features/services/blocs/services_cubit.dart b/lib/features/services/blocs/services_cubit.dart index ddc82e8..3d62ca4 100644 --- a/lib/features/services/blocs/services_cubit.dart +++ b/lib/features/services/blocs/services_cubit.dart @@ -16,7 +16,7 @@ part 'services_state.dart'; class ServicesCubit extends Cubit { final ServicesRepository _repository = GetIt.I(); - final SessionBloc _sessionBloc = GetIt.I(); + final SessionCubit _sessionCubit = GetIt.I(); ServicesCubit() : super(const ServicesState(status: ServicesStatus.initial)); @@ -41,7 +41,7 @@ class ServicesCubit extends Cubit { try { final currentOffset = refresh ? 0 : state.allServices.length; - final companyId = _sessionBloc.state.company?.id; + final companyId = _sessionCubit.state.company?.id; if (companyId == null) { throw Exception("Company ID non trovato nella sessione"); @@ -126,10 +126,10 @@ class ServicesCubit extends Cubit { emit( state.copyWith( currentService: ServiceModel( - storeId: _sessionBloc.state.selectedStore?.id ?? '', + storeId: _sessionCubit.state.currentStore?.id ?? '', number: '', // Sarà compilato dall'utente createdAt: DateTime.now(), - companyId: _sessionBloc.state.company!.id, + companyId: _sessionCubit.state.company!.id!, ), status: ServicesStatus.ready, ), diff --git a/lib/features/services/data/services_repository.dart b/lib/features/services/data/services_repository.dart index 52fa788..a20715d 100644 --- a/lib/features/services/data/services_repository.dart +++ b/lib/features/services/data/services_repository.dart @@ -9,7 +9,7 @@ import '../models/service_model.dart'; class ServicesRepository { final _supabase = Supabase.instance.client; - final companyId = GetIt.I.get().state.company!.id; + final companyId = GetIt.I.get().state.company!.id; final CustomerRepository _customerRepository = GetIt.I(); // --- RECUPERO SINGOLO SERVIZIO CON JOIN COMPLETO --- diff --git a/lib/features/services/ui/service_form_screen/entertainment_service_card.dart b/lib/features/services/ui/service_form_screen/entertainment_service_card.dart index e7fe8ab..4e07001 100644 --- a/lib/features/services/ui/service_form_screen/entertainment_service_card.dart +++ b/lib/features/services/ui/service_form_screen/entertainment_service_card.dart @@ -281,7 +281,7 @@ class _EntertainmentFormState extends State<_EntertainmentForm> { // Suggerimenti rapidi (Chip) FutureBuilder>( future: GetIt.I().fetchTopEntertainmentTypes( - GetIt.I().state.company!.id, + GetIt.I().state.company!.id!, ), builder: (context, snapshot) { final suggestions = snapshot.data ?? ["Netflix", "DAZN", "Sky"]; diff --git a/lib/features/services/utils/service_actions.dart b/lib/features/services/utils/service_actions.dart index 5f826b6..3159591 100644 --- a/lib/features/services/utils/service_actions.dart +++ b/lib/features/services/utils/service_actions.dart @@ -8,8 +8,8 @@ import 'package:go_router/go_router.dart'; /// Avvia la creazione di un nuovo servizio partendo dalla selezione dell'operatore. void startNewService(BuildContext context) { - final session = context.read().state; - final currentStoreId = session.selectedStore?.id; + final session = context.read().state; + final currentStoreId = session.currentStore?.id; if (currentStoreId == null) { ScaffoldMessenger.of(context).showSnackBar( @@ -59,7 +59,7 @@ void startNewService(BuildContext context) { employeeId: member.id, number: '', createdAt: DateTime.now(), - companyId: session.company!.id, + companyId: session.company!.id!, ), );