From 753b5489b6b4e7068eba56b328dabba8516e798f Mon Sep 17 00:00:00 2001 From: Mark M2 Macbook Date: Wed, 15 Apr 2026 10:05:07 +0200 Subject: [PATCH] Refactor StoreBloc to Cubit and Fix Staff Assignment UI (#1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Convertito StoreBloc in StoreCubit per coerenza con il resto del progetto. Sistemata la logica di assegnazione dipendenti nel modal dei negozi. Utilizzato il doppio BlocBuilder per garantire la reattività tra StaffCubit e StoreCubit. Reviewed-on: http://catelliub.zapto.org:3000/brontomark/flux/pulls/1 Co-authored-by: Mark M2 Macbook Co-committed-by: Mark M2 Macbook --- .../anagrafiche/master_data_hub_content.dart | 115 ------- .../master_data/staff/ui/staff_screen.dart | 6 +- .../master_data/store/bloc/store_bloc.dart | 56 ---- .../master_data/store/bloc/store_cubit.dart | 97 ++++++ .../master_data/store/bloc/store_events.dart | 18 -- .../master_data/store/bloc/store_state.dart | 14 +- .../store/ui/create_store_screen.dart | 6 +- .../master_data/store/ui/stores_screen.dart | 96 +++--- lib/features/staff/blocs/staff_cubit.dart | 110 ------- lib/features/staff/blocs/staff_state.dart | 33 -- lib/features/staff/data/staff_repository.dart | 92 ------ .../staff/models/staff_member_model.dart | 47 --- lib/features/staff/ui/staff_screen.dart | 292 ----------------- lib/features/store/bloc/store_bloc.dart | 68 ---- lib/features/store/bloc/store_events.dart | 18 -- lib/features/store/bloc/store_state.dart | 44 --- lib/features/store/data/store_repository.dart | 106 ------- lib/features/store/models/store_model.dart | 102 ------ .../store/ui/create_store_screen.dart | 245 --------------- lib/features/store/ui/stores_screen.dart | 297 ------------------ lib/main.dart | 8 +- 21 files changed, 170 insertions(+), 1700 deletions(-) delete mode 100644 lib/features/anagrafiche/master_data_hub_content.dart delete mode 100644 lib/features/master_data/store/bloc/store_bloc.dart create mode 100644 lib/features/master_data/store/bloc/store_cubit.dart delete mode 100644 lib/features/master_data/store/bloc/store_events.dart delete mode 100644 lib/features/staff/blocs/staff_cubit.dart delete mode 100644 lib/features/staff/blocs/staff_state.dart delete mode 100644 lib/features/staff/data/staff_repository.dart delete mode 100644 lib/features/staff/models/staff_member_model.dart delete mode 100644 lib/features/staff/ui/staff_screen.dart delete mode 100644 lib/features/store/bloc/store_bloc.dart delete mode 100644 lib/features/store/bloc/store_events.dart delete mode 100644 lib/features/store/bloc/store_state.dart delete mode 100644 lib/features/store/data/store_repository.dart delete mode 100644 lib/features/store/models/store_model.dart delete mode 100644 lib/features/store/ui/create_store_screen.dart delete mode 100644 lib/features/store/ui/stores_screen.dart diff --git a/lib/features/anagrafiche/master_data_hub_content.dart b/lib/features/anagrafiche/master_data_hub_content.dart deleted file mode 100644 index 97bc343..0000000 --- a/lib/features/anagrafiche/master_data_hub_content.dart +++ /dev/null @@ -1,115 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flux/core/theme/theme.dart'; -import 'package:flux/features/customers/ui/customers_content.dart'; -import 'package:flux/features/products/ui/products_screen.dart'; -import 'package:flux/features/staff/ui/staff_screen.dart'; -import 'package:flux/features/store/ui/stores_screen.dart'; - -class MasterDataHubContent extends StatelessWidget { - final Function(Widget) onOpenPage; - - const MasterDataHubContent({super.key, required this.onOpenPage}); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(24.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Anagrafiche", - style: TextStyle( - fontSize: 28, - fontWeight: FontWeight.bold, - color: context.accent, - ), - ), - const SizedBox(height: 8), - Text( - "Gestisci i dati fondamentali del tuo business", - style: TextStyle(color: context.secondaryText), - ), - const SizedBox(height: 32), - - Expanded( - child: GridView.count( - crossAxisCount: MediaQuery.of(context).size.width > 600 ? 3 : 2, - mainAxisSpacing: 16, - crossAxisSpacing: 16, - children: [ - _HubCard( - label: 'Prodotti', - icon: Icons.inventory_2_outlined, - color: Colors.blue, - onTap: () => onOpenPage( - const ProductsScreen(), - ), // Apre ProductsScreen, // Indice per ProductsScreen - ), - _HubCard( - label: 'Clienti', - icon: Icons.people_outlined, - color: Colors.orange, - onTap: () => onOpenPage( - const CustomersContent(), - ), // Indice per CustomersContent - ), - _HubCard( - label: 'Commessi', - icon: Icons.badge_outlined, - color: Colors.teal, - onTap: () => onOpenPage(const StaffScreen()), // Coming soon - ), - _HubCard( - label: 'Negozi', - icon: Icons.storefront_outlined, - color: Colors.purple, - onTap: () => onOpenPage(const StoresScreen()), // Coming soon - ), - ], - ), - ), - ], - ), - ); - } -} - -// Widget semplice per le card dell'hub -class _HubCard extends StatelessWidget { - final String label; - final IconData icon; - final Color color; - final VoidCallback onTap; - - const _HubCard({ - required this.label, - required this.icon, - required this.color, - required this.onTap, - }); - - @override - Widget build(BuildContext context) { - return Card( - elevation: 0, - color: context.background, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - side: BorderSide(color: context.accent.withValues(alpha: 0.1)), - ), - child: InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(16), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(icon, color: color, size: 40), - const SizedBox(height: 12), - Text(label, style: const TextStyle(fontWeight: FontWeight.bold)), - ], - ), - ), - ); - } -} diff --git a/lib/features/master_data/staff/ui/staff_screen.dart b/lib/features/master_data/staff/ui/staff_screen.dart index 8684f2a..a8c0a93 100644 --- a/lib/features/master_data/staff/ui/staff_screen.dart +++ b/lib/features/master_data/staff/ui/staff_screen.dart @@ -5,7 +5,7 @@ import 'package:flux/core/theme/theme.dart'; import 'package:flux/core/widgets/flux_text_field.dart'; import 'package:flux/features/master_data/staff/blocs/staff_cubit.dart'; // Tuo percorso import 'package:flux/features/master_data/staff/models/staff_member_model.dart'; -import 'package:flux/features/master_data/store/bloc/store_bloc.dart'; +import 'package:flux/features/master_data/store/bloc/store_cubit.dart'; class StaffScreen extends StatefulWidget { const StaffScreen({super.key}); @@ -94,7 +94,7 @@ class _StaffScreenState extends State { } Widget _buildStoreSelector() { - return BlocBuilder( + return BlocBuilder( // Assumendo tu abbia uno StoreCubit builder: (context, state) { return Padding( @@ -222,7 +222,7 @@ class _StaffScreenState extends State { // --- SELETTORE NEGOZI (CHIPS) --- // Qui usiamo il BlocBuilder per i negozi, ma il setModalState per il refresh - BlocBuilder( + BlocBuilder( builder: (context, storeState) { if (storeState.status == StoreStatus.loading) { return const CircularProgressIndicator(); diff --git a/lib/features/master_data/store/bloc/store_bloc.dart b/lib/features/master_data/store/bloc/store_bloc.dart deleted file mode 100644 index 0fa8dbd..0000000 --- a/lib/features/master_data/store/bloc/store_bloc.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:equatable/equatable.dart'; -import 'package:flux/core/blocs/session/session_bloc.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'; - -part 'store_events.dart'; -part 'store_state.dart'; - -class StoreBloc extends Bloc { - final StoreRepository _repository = GetIt.I(); - final SessionBloc _sessionBloc; - - StoreBloc(this._sessionBloc) : super(const StoreState(stores: [])) { - on(_onCreateStore); - on(_onLoadStores); - } - - Future _onCreateStore( - CreateStoreRequested event, - Emitter emit, - ) async { - emit(state.copyWith(status: StoreStatus.loading)); - try { - await _repository.createStore(event.store); - emit(state.copyWith(status: StoreStatus.success)); - } catch (e) { - emit( - state.copyWith(status: StoreStatus.failure, errorMessage: e.toString()), - ); - } - } - - Future _onLoadStores( - LoadStoresRequested event, - Emitter emit, - ) async { - emit(state.copyWith(status: StoreStatus.loading)); - try { - final stores = await _repository.getStoresByCompany( - _sessionBloc.state.company!.id, - ); - emit( - state.copyWith( - status: StoreStatus.success, - stores: stores, // Assicurati di avere 'stores' nello StoreState - ), - ); - } catch (e) { - emit( - state.copyWith(status: StoreStatus.failure, errorMessage: e.toString()), - ); - } - } -} diff --git a/lib/features/master_data/store/bloc/store_cubit.dart b/lib/features/master_data/store/bloc/store_cubit.dart new file mode 100644 index 0000000..b6bfc30 --- /dev/null +++ b/lib/features/master_data/store/bloc/store_cubit.dart @@ -0,0 +1,97 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flux/core/blocs/session/session_bloc.dart'; +import 'package:flux/features/master_data/staff/data/staff_repository.dart'; +import 'package:flux/features/master_data/staff/models/staff_member_model.dart'; +import 'package:flux/features/master_data/store/data/store_repository.dart'; +import 'package:flux/features/master_data/store/models/store_model.dart'; +import 'package:get_it/get_it.dart'; + +part 'store_state.dart'; + +class StoreCubit extends Cubit { + final StoreRepository _repository = GetIt.I(); + final StaffRepository _staffRepository = GetIt.I(); + final SessionBloc _sessionBloc; + + StoreCubit(this._sessionBloc) : super(const StoreState(stores: [])); + + Future createStore(final StoreModel store) async { + emit(state.copyWith(status: StoreStatus.loading)); + try { + await _repository.createStore(store); + emit(state.copyWith(status: StoreStatus.success)); + } catch (e) { + emit( + state.copyWith(status: StoreStatus.failure, errorMessage: e.toString()), + ); + } + } + + Future loadStores() async { + emit(state.copyWith(status: StoreStatus.loading)); + try { + final stores = await _repository.getStoresByCompany( + _sessionBloc.state.company!.id, + ); + final Map> staffByStore = {}; + for (StoreModel store in stores) { + staffByStore[store.id!] = await _staffRepository.getStaffMembersInStore( + store.id!, + ); + } + emit( + state.copyWith( + status: StoreStatus.success, + stores: stores, + staffByStore: staffByStore, + ), + ); + } catch (e) { + emit( + state.copyWith(status: StoreStatus.failure, errorMessage: e.toString()), + ); + } + } + + Future assignStaffToStore(String storeId, String staffId) async { + try { + await _staffRepository.assignToStore(staffId, storeId); + // Dopo l'assegnazione, potresti voler ricaricare lo staff per quel negozio + final updatedStaff = await _staffRepository.getStaffMembersInStore( + storeId, + ); + final newMap = Map>.from( + state.staffByStore, + ); + newMap[storeId] = updatedStaff; + emit(state.copyWith(status: StoreStatus.success, staffByStore: newMap)); + } catch (e) { + emit( + state.copyWith(status: StoreStatus.failure, errorMessage: e.toString()), + ); + } + } + + // Rimuove un dipendente da un negozio + Future removeStaffFromStore(String staffId, String storeId) async { + try { + await _staffRepository.removeFromStore(staffId, storeId); + final updatedStaff = await _staffRepository.getStaffMembersInStore( + storeId, + ); + final newMap = Map>.from( + state.staffByStore, + ); + newMap[storeId] = updatedStaff; + emit(state.copyWith(staffByStore: newMap)); + } catch (e) { + emit( + state.copyWith( + status: StoreStatus.failure, + errorMessage: "Errore nella rimozione: $e", + ), + ); + } + } +} diff --git a/lib/features/master_data/store/bloc/store_events.dart b/lib/features/master_data/store/bloc/store_events.dart deleted file mode 100644 index bc13f5a..0000000 --- a/lib/features/master_data/store/bloc/store_events.dart +++ /dev/null @@ -1,18 +0,0 @@ -part of 'store_bloc.dart'; - -abstract class StoreEvent extends Equatable { - const StoreEvent(); - - @override - List get props => []; -} - -class CreateStoreRequested extends StoreEvent { - final StoreModel store; - const CreateStoreRequested({required this.store}); - - @override - List get props => [store]; -} - -class LoadStoresRequested extends StoreEvent {} diff --git a/lib/features/master_data/store/bloc/store_state.dart b/lib/features/master_data/store/bloc/store_state.dart index 38a1c01..772e8c6 100644 --- a/lib/features/master_data/store/bloc/store_state.dart +++ b/lib/features/master_data/store/bloc/store_state.dart @@ -1,4 +1,4 @@ -part of 'store_bloc.dart'; +part of 'store_cubit.dart'; enum StoreStatus { initial, loading, success, failure } @@ -7,12 +7,14 @@ class StoreState extends Equatable { final StoreModel? store; final String? errorMessage; final List stores; + final Map> staffByStore; const StoreState({ this.status = StoreStatus.initial, this.store, this.errorMessage, required this.stores, + this.staffByStore = const {}, }); StoreState copyWith({ @@ -20,15 +22,23 @@ class StoreState extends Equatable { StoreModel? store, String? errorMessage, List? stores, + Map>? staffByStore, }) { return StoreState( status: status ?? this.status, store: store ?? this.store, errorMessage: errorMessage ?? this.errorMessage, stores: stores ?? this.stores, + staffByStore: staffByStore ?? this.staffByStore, ); } @override - List get props => [status, store, errorMessage, stores]; + List get props => [ + status, + store, + errorMessage, + stores, + staffByStore, + ]; } 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 0526aae..1b3c9e8 100644 --- a/lib/features/master_data/store/ui/create_store_screen.dart +++ b/lib/features/master_data/store/ui/create_store_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/features/master_data/store/bloc/store_bloc.dart'; +import 'package:flux/features/master_data/store/bloc/store_cubit.dart'; import 'package:flux/features/master_data/store/models/store_model.dart'; import 'package:flux/core/blocs/session/session_bloc.dart'; import 'package:flux/core/theme/theme.dart'; @@ -76,7 +76,7 @@ class _CreateStoreScreenState extends State { provincia: _provinciaController.text.trim().toUpperCase(), ); - context.read().add(CreateStoreRequested(store: store)); + context.read().createStore(store); } } @@ -84,7 +84,7 @@ class _CreateStoreScreenState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Il tuo primo Negozio')), - body: BlocConsumer( + body: BlocConsumer( listener: (context, state) { if (state.status == StoreStatus.success) { context.read().add(AppStarted()); diff --git a/lib/features/master_data/store/ui/stores_screen.dart b/lib/features/master_data/store/ui/stores_screen.dart index 3e38544..5b23524 100644 --- a/lib/features/master_data/store/ui/stores_screen.dart +++ b/lib/features/master_data/store/ui/stores_screen.dart @@ -4,7 +4,7 @@ import 'package:flux/core/blocs/session/session_bloc.dart'; import 'package:flux/core/theme/theme.dart'; import 'package:flux/core/widgets/flux_text_field.dart'; import 'package:flux/features/master_data/staff/blocs/staff_cubit.dart'; -import 'package:flux/features/master_data/store/bloc/store_bloc.dart'; +import 'package:flux/features/master_data/store/bloc/store_cubit.dart'; import 'package:flux/features/master_data/store/models/store_model.dart'; class StoresScreen extends StatefulWidget { @@ -19,7 +19,7 @@ class _StoresScreenState extends State { void initState() { super.initState(); // Carichiamo i negozi e anche lo staff (per poterlo assegnare) - context.read().add(LoadStoresRequested()); + context.read().loadStores(); context.read().loadAllStaff(); } @@ -27,7 +27,7 @@ class _StoresScreenState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("I Tuoi Negozi")), - body: BlocBuilder( + body: BlocBuilder( builder: (context, state) { if (state.status == StoreStatus.loading) { return const Center(child: CircularProgressIndicator()); @@ -90,10 +90,10 @@ class _StoresScreenState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ // Mostra quanti dipendenti ci sono (usando lo StaffCubit) - BlocBuilder( - builder: (context, staffState) { + BlocBuilder( + builder: (context, storeState) { final staffCount = - staffState.storesByStaff[store.id]?.length ?? 0; + storeState.staffByStore[store.id]?.length ?? 0; return ActionChip( avatar: const Icon(Icons.people, size: 16), label: Text("$staffCount Dipendenti"), @@ -119,42 +119,52 @@ class _StoresScreenState extends State { context: context, isScrollControlled: true, builder: (context) => BlocBuilder( - builder: (context, state) { - return Container( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text("Personale di ${store.nome}", style: context.titleLarge), - const SizedBox(height: 16), - // Lista di TUTTO lo staff dell'azienda con checkbox - ...state.allStaff.map((person) { - final bool isAssigned = - state.storesByStaff[store.id!]?.any( - (s) => s.id == person.id, - ) ?? - false; + // 1. Prendi TUTTI i dipendenti + builder: (context, staffState) { + return BlocBuilder( + // 2. Prendi le ASSEGNAZIONI + builder: (context, storeState) { + final assignedToThisStore = + storeState.staffByStore[store.id!] ?? []; - return CheckboxListTile( - title: Text(person.name), - value: isAssigned, - onChanged: (selected) { - if (selected == true) { - context.read().assignMemberToStore( - person.id!, - store.id!, - ); - } else { - context.read().removeMemberFromStore( - person.id!, - store.id!, - ); - } - }, - ); - }), - ], - ), + return Container( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "Personale di ${store.nome}", + style: context.titleLarge, + ), + const SizedBox(height: 16), + ...staffState.allStaff.map((person) { + // La spunta deve dipendere dallo StoreCubit! + final bool isAssigned = assignedToThisStore.any( + (s) => s.id == person.id, + ); + + return CheckboxListTile( + title: Text(person.name), + value: isAssigned, + onChanged: (selected) { + if (selected == true) { + context.read().assignStaffToStore( + store.id!, + person.id!, + ); + } else { + context.read().removeStaffFromStore( + person.id!, + store.id!, + ); + } + }, + ); + }), + ], + ), + ); + }, ); }, ), @@ -279,9 +289,7 @@ class _StoresScreenState extends State { ); // Chiamata al Bloc per il salvataggio - context.read().add( - CreateStoreRequested(store: storeData), - ); + context.read().createStore(storeData); Navigator.pop(context); }, diff --git a/lib/features/staff/blocs/staff_cubit.dart b/lib/features/staff/blocs/staff_cubit.dart deleted file mode 100644 index c1f27c3..0000000 --- a/lib/features/staff/blocs/staff_cubit.dart +++ /dev/null @@ -1,110 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/core/blocs/session/session_bloc.dart'; -import 'package:flux/features/staff/data/staff_repository.dart'; -import 'package:flux/features/staff/models/staff_member_model.dart'; -import 'package:get_it/get_it.dart'; - -part 'staff_state.dart'; - -class StaffCubit extends Cubit { - final StaffRepository _repository = GetIt.I.get(); - final SessionBloc _sessionBloc; - - StaffCubit(this._sessionBloc) : super(const StaffState()); - - // Carica tutto lo staff della compagnia - Future loadAllStaff() async { - emit(state.copyWith(isLoading: true, error: null)); - try { - final staff = await _repository.getStaffMembers( - _sessionBloc.state.company!.id, - ); - emit(state.copyWith(allStaff: staff, isLoading: false)); - } catch (e) { - emit(state.copyWith(isLoading: false, error: e.toString())); - } - } - - // Carica lo staff di uno specifico negozio e aggiorna la mappa - Future loadStaffForStore(String storeId) async { - try { - final staffInStore = await _repository.getStaffMembersInStore(storeId); - final newMap = Map>.from( - state.staffByStore, - ); - newMap[storeId] = staffInStore; - emit(state.copyWith(staffByStore: newMap)); - } catch (e) { - // Qui potresti gestire l'errore silenziosamente per non bloccare tutta l'UI - } - } - - // Salva o aggiorna un membro - Future saveStaffMember(StaffMemberModel member) async { - emit(state.copyWith(isLoading: true)); - try { - await _repository.saveStaffMember(member); - await loadAllStaff(); // Ricarichiamo la lista aggiornata - } catch (e) { - emit(state.copyWith(isLoading: false, error: e.toString())); - } - } - - // Associa un dipendente a un negozio - Future assignMemberToStore(String staffId, String storeId) async { - try { - await _repository.assignToStore(staffId, storeId); - await loadStaffForStore(storeId); // Aggiorna solo quel negozio nell'UI - } catch (e) { - emit(state.copyWith(error: "Errore nell'assegnazione: $e")); - } - } - - // Rimuove un dipendente da un negozio - Future removeMemberFromStore(String staffId, String storeId) async { - try { - await _repository.removeFromStore(staffId, storeId); - await loadStaffForStore(storeId); - } catch (e) { - emit(state.copyWith(error: "Errore nella rimozione: $e")); - } - } - - Future saveStaffWithStores({ - required StaffMemberModel member, - required List selectedStoreIds, - }) async { - emit(state.copyWith(isLoading: true)); - try { - // 1. Salva o aggiorna l'anagrafica (ci serve l'ID) - // Se è un nuovo membro, Supabase ci restituirà l'ID generato - final savedMember = await _repository.saveStaffMember(member); - final String staffId = savedMember.id!; - - // 2. Sincronizzazione Negozi - // Per semplicità e pulizia, rimuoviamo le vecchie assegnazioni e inseriamo le nuove - // (Oppure facciamo un confronto tra liste, ma il reset & rewrite è più sicuro qui) - await _repository.clearStoreAssignments(staffId); - - if (selectedStoreIds.isNotEmpty) { - await Future.wait( - selectedStoreIds.map( - (storeId) => _repository.assignToStore(staffId, storeId), - ), - ); - } - - // 3. Rinfresca i dati - await loadAllStaff(); - // Aggiorniamo anche lo stato dei negozi coinvolti - for (var storeId in selectedStoreIds) { - await loadStaffForStore(storeId); - } - - emit(state.copyWith(isLoading: false)); - } catch (e) { - emit(state.copyWith(isLoading: false, error: e.toString())); - } - } -} diff --git a/lib/features/staff/blocs/staff_state.dart b/lib/features/staff/blocs/staff_state.dart deleted file mode 100644 index 079aed1..0000000 --- a/lib/features/staff/blocs/staff_state.dart +++ /dev/null @@ -1,33 +0,0 @@ -part of 'staff_cubit.dart'; - -class StaffState extends Equatable { - final List allStaff; - final Map> - staffByStore; // storeId -> List of staff - final bool isLoading; - final String? error; - - const StaffState({ - this.allStaff = const [], - this.staffByStore = const {}, - this.isLoading = false, - this.error, - }); - - StaffState copyWith({ - List? allStaff, - Map>? staffByStore, - bool? isLoading, - String? error, - }) { - return StaffState( - allStaff: allStaff ?? this.allStaff, - staffByStore: staffByStore ?? this.staffByStore, - isLoading: isLoading ?? this.isLoading, - error: error, - ); - } - - @override - List get props => [allStaff, staffByStore, isLoading, error]; -} diff --git a/lib/features/staff/data/staff_repository.dart b/lib/features/staff/data/staff_repository.dart deleted file mode 100644 index 982e58c..0000000 --- a/lib/features/staff/data/staff_repository.dart +++ /dev/null @@ -1,92 +0,0 @@ -import 'package:flux/features/staff/models/staff_member_model.dart'; -import 'package:flux/features/store/models/store_model.dart'; -import 'package:get_it/get_it.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; - -class StaffRepository { - final SupabaseClient _supabase = GetIt.I.get(); - - // --- ANAGRAFICA PURA --- - - // Prende tutto lo staff della Company (per l'Hub Anagrafiche) - Future> getStaffMembers(String companyId) async { - final response = await _supabase - .from('staff_member') - .select() - .eq('company_id', companyId) - .order('name', ascending: true); - - return (response as List).map((s) => StaffMemberModel.fromJson(s)).toList(); - } - - Future saveStaffMember(StaffMemberModel member) async { - final response = await _supabase - .from('staff_member') - .upsert(member.toJson()) - .select() - .single(); - - return StaffMemberModel.fromJson(response); - } - - // --- LOGICA DI GIUNZIONE (Staff <-> Store) --- - - // Recupera i membri assegnati a uno specifico negozio - // Qui facciamo una JOIN per avere i dati del membro partendo dalla tabella di giunzione - Future> getStaffMembersInStore(String storeId) async { - final response = await _supabase - .from('staff_in_stores') - .select( - 'staff_member (*)', - ) // Prende tutti i campi della tabella staff_member collegata - .eq('store_id', storeId); - - return (response as List) - .map((item) => StaffMemberModel.fromJson(item['staff_member'])) - .toList(); - } - - // Recupera i negozi associati ad un specifico membro - // Qui facciamo una JOIN per avere i dati del membro partendo dalla tabella di giunzione - Future> getStoresWithStaffMember( - String staffMemberId, - ) async { - final response = await _supabase - .from('staff_in_stores') - .select( - 'store (*)', - ) // Prende tutti i campi della tabella store collegata - .eq('staff_member_id', staffMemberId); - - return (response as List) - .map((item) => StoreModel.fromJson(item['store'])) - .toList(); - } - - // Assegna un membro a un negozio - Future assignToStore(String staffId, String storeId) async { - await _supabase.from('staff_in_stores').insert({ - 'staff_member_id': staffId, - 'store_id': storeId, - }); - } - - // Rimuove l'assegnazione - Future removeFromStore(String staffId, String storeId) async { - await _supabase - .from('staff_in_stores') - .delete() - .eq('staff_member_id', staffId) - .eq('store_id', storeId); - } - - // Nel StaffRepository - - // Utility per pulire le assegnazioni esistenti prima di riscriverle - Future clearStoreAssignments(String staffId) async { - await _supabase - .from('staff_in_stores') - .delete() - .eq('staff_member_id', staffId); - } -} diff --git a/lib/features/staff/models/staff_member_model.dart b/lib/features/staff/models/staff_member_model.dart deleted file mode 100644 index 425d901..0000000 --- a/lib/features/staff/models/staff_member_model.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:flux/core/utils/string_extensions.dart'; // Assicurati che il percorso sia corretto - -class StaffMemberModel extends Equatable { - final String? id; - final String name; - final String email; - final String phone; - final bool isActive; - final String companyId; - - const StaffMemberModel({ - this.id, - required this.name, - this.email = '', - this.phone = '', - this.isActive = true, - required this.companyId, - }); - - factory StaffMemberModel.fromJson(Map json) { - return StaffMemberModel( - id: json['id'], - // Applichiamo il tuo myFormat per visualizzare i nomi correttamente - name: (json['name'] as String).myFormat(), - // L'email la teniamo lowercase per standard tecnico - email: (json['email'] as String? ?? '').toLowerCase().trim(), - phone: (json['phone'] as String? ?? '').trim(), - isActive: json['is_active'] ?? true, - companyId: json['company_id'], - ); - } - - Map toJson() { - return { - if (id != null) 'id': id, - 'name': name.toLowerCase().trim(), // Salviamo pulito per le query - 'email': email.toLowerCase().trim(), - 'phone': phone.trim(), - 'is_active': isActive, - 'company_id': companyId, - }; - } - - @override - List get props => [id, name, email, phone, isActive, companyId]; -} diff --git a/lib/features/staff/ui/staff_screen.dart b/lib/features/staff/ui/staff_screen.dart deleted file mode 100644 index dcdb339..0000000 --- a/lib/features/staff/ui/staff_screen.dart +++ /dev/null @@ -1,292 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/core/blocs/session/session_bloc.dart'; -import 'package:flux/core/theme/theme.dart'; -import 'package:flux/core/widgets/flux_text_field.dart'; -import 'package:flux/features/staff/blocs/staff_cubit.dart'; // Tuo percorso -import 'package:flux/features/staff/models/staff_member_model.dart'; -import 'package:flux/features/store/bloc/store_bloc.dart'; - -class StaffScreen extends StatefulWidget { - const StaffScreen({super.key}); - - @override - State createState() => _StaffScreenState(); -} - -class _StaffScreenState extends State { - String? _selectedStoreId; - bool _showAllCompanyStaff = false; // Partiamo con la vista globale - - @override - void initState() { - super.initState(); - // Carichiamo subito tutto - _selectedStoreId = context.read().state.selectedStore!.id; - context.read().loadStaffForStore(_selectedStoreId!); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: context.background, - appBar: AppBar( - title: const Text("Anagrafica Personale"), - actions: [ - // Toggle per vista Azienda / Negozio - Padding( - padding: const EdgeInsets.only(right: 16), - child: FilterChip( - label: const Text("Tutta l'Azienda"), - selected: _showAllCompanyStaff, - onSelected: (val) => setState(() => _showAllCompanyStaff = val), - selectedColor: context.accent.withValues(alpha: 0.2), - ), - ), - ], - ), - body: Column( - children: [ - // --- BARRA FILTRO NEGOZIO (Visibile solo se non 'Tutta l'Azienda') --- - AnimatedContainer( - duration: const Duration(milliseconds: 300), - height: _showAllCompanyStaff ? 0 : 80, - child: _showAllCompanyStaff - ? const SizedBox() - : _buildStoreSelector(), - ), - - // --- LISTA PERSONALE --- - Expanded( - child: BlocBuilder( - builder: (context, state) { - final list = _showAllCompanyStaff - ? state.allStaff - : (state.staffByStore[_selectedStoreId] ?? []); - - if (state.isLoading && list.isEmpty) { - return const Center(child: CircularProgressIndicator()); - } - - if (list.isEmpty) { - return _buildEmptyState(); - } - - return ListView.separated( - padding: const EdgeInsets.all(16), - itemCount: list.length, - separatorBuilder: (_, _) => const SizedBox(height: 12), - itemBuilder: (context, index) { - final member = list[index]; - return _buildStaffCard(member); - }, - ); - }, - ), - ), - ], - ), - floatingActionButton: FloatingActionButton.extended( - onPressed: () => _openStaffForm(context), - label: const Text("Aggiungi"), - icon: const Icon(Icons.person_add_alt_1), - ), - ); - } - - Widget _buildStoreSelector() { - return BlocBuilder( - // Assumendo tu abbia uno StoreCubit - builder: (context, state) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: DropdownButtonFormField( - initialValue: _selectedStoreId, - decoration: const InputDecoration(labelText: "Filtra per Negozio"), - items: state.stores - .map((s) => DropdownMenuItem(value: s.id, child: Text(s.nome))) - .toList(), - onChanged: (id) { - setState(() => _selectedStoreId = id); - if (id != null) context.read().loadStaffForStore(id); - }, - ), - ); - }, - ); - } - - Widget _buildStaffCard(StaffMemberModel member) { - return Card( - elevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - side: BorderSide(color: context.accent.withValues(alpha: 0.1)), - ), - child: ListTile( - contentPadding: const EdgeInsets.all(12), - leading: CircleAvatar( - backgroundColor: context.accent.withValues(alpha: 0.1), - child: Text(member.name[0], style: TextStyle(color: context.accent)), - ), - title: Text( - member.name, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (member.email.isNotEmpty) Text(member.email), - Text(member.phone.isNotEmpty ? member.phone : "Nessun telefono"), - ], - ), - trailing: const Icon(Icons.edit_note), - onTap: () => _openStaffForm(context, member: member), - ), - ); - } - - void _openStaffForm(BuildContext context, {StaffMemberModel? member}) { - final nameController = TextEditingController(text: member?.name); - final emailController = TextEditingController(text: member?.email); - final phoneController = TextEditingController(text: member?.phone); - - // Lista temporanea per le chip (se è un edit, andrebbe popolata con le assegnazioni attuali) - List tempSelectedStores = []; - - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (context) => StatefulBuilder( - // Necessario per le chip dentro il BottomSheet - builder: (context, setModalState) => Container( - decoration: BoxDecoration( - color: Theme.of(context).scaffoldBackgroundColor, - borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), - ), - padding: EdgeInsets.only( - top: 24, - left: 24, - right: 24, - bottom: MediaQuery.of(context).viewInsets.bottom + 24, - ), - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - member == null - ? "Nuovo Collaboratore" - : "Modifica Collaboratore", - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 24), - FluxTextField( - controller: nameController, - label: "Nome e Cognome", - icon: Icons.person, - ), - const SizedBox(height: 16), - FluxTextField( - controller: emailController, - label: "Email", - icon: Icons.email, - ), - const SizedBox(height: 16), - FluxTextField( - controller: phoneController, - label: "Telefono", - icon: Icons.phone, - ), - const SizedBox(height: 24), - - const Text( - "Assegna ai Negozi", - style: TextStyle(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 12), - - // --- SELETTORE NEGOZI (CHIPS) --- - BlocBuilder( - builder: (context, state) { - return Wrap( - spacing: 8, - runSpacing: 8, - children: state.stores.map((store) { - final isSelected = tempSelectedStores.contains( - store.id, - ); - return FilterChip( - label: Text(store.nome), - selected: isSelected, - onSelected: (selected) { - setModalState(() { - selected - ? tempSelectedStores.add(store.id!) - : tempSelectedStores.remove(store.id); - }); - }, - selectedColor: context.accent.withValues(alpha: 0.2), - checkmarkColor: context.accent, - ); - }).toList(), - ); - }, - ), - - const SizedBox(height: 32), - SizedBox( - width: double.infinity, - height: 50, - child: ElevatedButton( - onPressed: () { - final newMember = StaffMemberModel( - id: member?.id, - name: nameController.text, - email: emailController.text, - phone: phoneController.text, - companyId: context - .read() - .state - .company! - .id, - ); - - context.read().saveStaffWithStores( - member: newMember, - selectedStoreIds: tempSelectedStores, - ); - Navigator.pop(context); - }, - child: const Text("SALVA COLLABORATORE"), - ), - ), - ], - ), - ), - ), - ), - ); - } - - Widget _buildEmptyState() { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.people_outline, size: 64, color: context.secondaryText), - const SizedBox(height: 16), - Text( - "Nessun membro trovato", - style: TextStyle(color: context.secondaryText), - ), - ], - ), - ); - } -} diff --git a/lib/features/store/bloc/store_bloc.dart b/lib/features/store/bloc/store_bloc.dart deleted file mode 100644 index 7502eb3..0000000 --- a/lib/features/store/bloc/store_bloc.dart +++ /dev/null @@ -1,68 +0,0 @@ -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:equatable/equatable.dart'; -import 'package:flux/core/blocs/session/session_bloc.dart'; -import 'package:flux/features/staff/models/staff_member_model.dart'; -import 'package:flux/features/store/data/store_repository.dart'; -import 'package:flux/features/store/models/store_model.dart'; -import 'package:get_it/get_it.dart'; - -part 'store_events.dart'; -part 'store_state.dart'; - -class StoreBloc extends Bloc { - final StoreRepository _repository = GetIt.I(); - final SessionBloc _sessionBloc; - - StoreBloc(this._sessionBloc) - : super(const StoreState(stores: [], staffByStore: {})) { - on(_onCreateStore); - on(_onLoadStores); - } - - Future _onCreateStore( - CreateStoreRequested event, - Emitter emit, - ) async { - emit(state.copyWith(status: StoreStatus.loading)); - try { - await _repository.createStore(event.store); - emit(state.copyWith(status: StoreStatus.success)); - } catch (e) { - emit( - state.copyWith(status: StoreStatus.failure, errorMessage: e.toString()), - ); - } - } - - Future _onLoadStores( - LoadStoresRequested event, - Emitter emit, - ) async { - emit(state.copyWith(status: StoreStatus.loading)); - try { - final stores = await _repository.getStoresByCompany( - _sessionBloc.state.company!.id, - ); - final staffByStore = >{}; - - for (final store in stores) { - final staff = await _repository.getStaffMembersInStore( - _sessionBloc.state.company!.id, - ); - staffByStore[store] = staff; - } - - emit( - state.copyWith( - status: StoreStatus.success, - stores: stores, // Assicurati di avere 'stores' nello StoreState - staffByStore: staffByStore, - ), - ); - } catch (e) { - emit( - state.copyWith(status: StoreStatus.failure, errorMessage: e.toString()), - ); - } - } -} diff --git a/lib/features/store/bloc/store_events.dart b/lib/features/store/bloc/store_events.dart deleted file mode 100644 index bc13f5a..0000000 --- a/lib/features/store/bloc/store_events.dart +++ /dev/null @@ -1,18 +0,0 @@ -part of 'store_bloc.dart'; - -abstract class StoreEvent extends Equatable { - const StoreEvent(); - - @override - List get props => []; -} - -class CreateStoreRequested extends StoreEvent { - final StoreModel store; - const CreateStoreRequested({required this.store}); - - @override - List get props => [store]; -} - -class LoadStoresRequested extends StoreEvent {} diff --git a/lib/features/store/bloc/store_state.dart b/lib/features/store/bloc/store_state.dart deleted file mode 100644 index 3add3ce..0000000 --- a/lib/features/store/bloc/store_state.dart +++ /dev/null @@ -1,44 +0,0 @@ -part of 'store_bloc.dart'; - -enum StoreStatus { initial, loading, success, failure } - -class StoreState extends Equatable { - final StoreStatus status; - final StoreModel? store; - final String? errorMessage; - final List stores; - final Map> staffByStore; - - const StoreState({ - this.status = StoreStatus.initial, - this.store, - this.errorMessage, - required this.stores, - required this.staffByStore, - }); - - StoreState copyWith({ - StoreStatus? status, - StoreModel? store, - String? errorMessage, - List? stores, - Map>? staffByStore, - }) { - return StoreState( - status: status ?? this.status, - store: store ?? this.store, - errorMessage: errorMessage ?? this.errorMessage, - stores: stores ?? this.stores, - staffByStore: staffByStore ?? this.staffByStore, - ); - } - - @override - List get props => [ - status, - store, - errorMessage, - stores, - staffByStore, - ]; -} diff --git a/lib/features/store/data/store_repository.dart b/lib/features/store/data/store_repository.dart deleted file mode 100644 index b4de907..0000000 --- a/lib/features/store/data/store_repository.dart +++ /dev/null @@ -1,106 +0,0 @@ -import 'package:flux/features/staff/models/staff_member_model.dart'; -import 'package:get_it/get_it.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; -import '../models/store_model.dart'; - -class StoreRepository { - final SupabaseClient _supabase = GetIt.I.get(); - - /// Crea un nuovo negozio associato alla compagnia dell'utente - Future createStore(StoreModel store) async { - try { - final response = await _supabase - .from('store') - .upsert(store.toJson()) - .select() - .single(); - - return StoreModel.fromJson(response); - } on PostgrestException catch (e) { - // Intercettiamo errori specifici del database - throw e.message; - } catch (e) { - throw 'Errore imprevisto durante la creazione del negozio: $e'; - } - } - - /// Recupera tutti i negozi di una determinata compagnia - Future> getStoresByCompany(String companyId) async { - try { - final response = await _supabase - .from('store') - .select() - .eq('company_id', companyId) - .order('created_at'); - - return (response as List) - .map((json) => StoreModel.fromJson(json)) - .toList(); - } catch (e) { - throw 'Errore nel recupero dei negozi'; - } - } - - Future updateStoreStatus(String id, bool isActive) async { - await _supabase.from('store').update({'is_active': isActive}).eq('id', id); - } - - // --- LOGICA DI GIUNZIONE (Staff <-> Store) --- - - // Recupera i membri assegnati a uno specifico negozio - // Qui facciamo una JOIN per avere i dati del membro partendo dalla tabella di giunzione - Future> getStaffMembersInStore(String storeId) async { - final response = await _supabase - .from('staff_in_stores') - .select( - 'staff_member (*)', - ) // Prende tutti i campi della tabella staff_member collegata - .eq('store_id', storeId); - - return (response as List) - .map((item) => StaffMemberModel.fromJson(item['staff_member'])) - .toList(); - } - - // Recupera i negozi associati ad un specifico membro - // Qui facciamo una JOIN per avere i dati del membro partendo dalla tabella di giunzione - Future> getStoresWithStaffMember( - String staffMemberId, - ) async { - final response = await _supabase - .from('staff_in_stores') - .select( - 'store (*)', - ) // Prende tutti i campi della tabella store collegata - .eq('staff_member_id', staffMemberId); - - return (response as List) - .map((item) => StoreModel.fromJson(item['store'])) - .toList(); - } - - // Assegna un negozio a un membro - Future assignToMember(String staffId, String storeId) async { - await _supabase.from('staff_in_stores').insert({ - 'staff_member_id': staffId, - 'store_id': storeId, - }); - } - - // Rimuove l'assegnazione - Future removeFromStore(String staffId, String storeId) async { - await _supabase - .from('staff_in_stores') - .delete() - .eq('staff_member_id', staffId) - .eq('store_id', storeId); - } - - // Utility per pulire le assegnazioni esistenti prima di riscriverle - Future clearStoreAssignments(String staffId) async { - await _supabase - .from('staff_in_stores') - .delete() - .eq('staff_member_id', staffId); - } -} diff --git a/lib/features/store/models/store_model.dart b/lib/features/store/models/store_model.dart deleted file mode 100644 index 680e2f2..0000000 --- a/lib/features/store/models/store_model.dart +++ /dev/null @@ -1,102 +0,0 @@ -import 'package:equatable/equatable.dart'; - -class StoreModel extends Equatable { - final String? id; - final String nome; - final String companyId; - final bool isActive; - final bool isPaid; - final DateTime? paymentExpiration; - final String indirizzo; - final String cap; - final String comune; - final String provincia; - - const StoreModel({ - this.id, - required this.nome, - required this.companyId, - this.isActive = true, - this.isPaid = false, - this.paymentExpiration, - required this.indirizzo, - required this.cap, - required this.comune, - required this.provincia, - }); - - // Fondamentale per Equatable: definisce quali proprietà determinano l'uguaglianza - @override - List get props => [ - id, - nome, - companyId, - isActive, - isPaid, - paymentExpiration, - indirizzo, - cap, - comune, - provincia, - ]; - - // Il mitico copyWith per creare nuove istanze modificando solo ciò che serve - StoreModel copyWith({ - String? id, - String? nome, - String? companyId, - bool? isActive, - bool? isPaid, - DateTime? paymentExpiration, - String? indirizzo, - String? cap, - String? comune, - String? provincia, - }) { - return StoreModel( - id: id ?? this.id, - nome: nome ?? this.nome, - companyId: companyId ?? this.companyId, - isActive: isActive ?? this.isActive, - isPaid: isPaid ?? this.isPaid, - paymentExpiration: paymentExpiration ?? this.paymentExpiration, - indirizzo: indirizzo ?? this.indirizzo, - cap: cap ?? this.cap, - comune: comune ?? this.comune, - provincia: provincia ?? this.provincia, - ); - } - - factory StoreModel.fromJson(Map json) { - return StoreModel( - id: json['id'] as String, - nome: json['nome'], - companyId: json['company_id'] as String, - isActive: json['is_active'] ?? true, - isPaid: json['is_paid'] ?? false, - paymentExpiration: json['payment_expiration'] != null - ? DateTime.parse(json['payment_expiration']) - : null, - indirizzo: json['indirizzo'], - cap: json['cap'], - comune: json['comune'], - provincia: json['provincia'], - ); - } - - Map toJson() { - return { - if (id != null) 'id': id, - 'nome': nome, - 'company_id': companyId, - 'is_active': isActive, - 'is_paid': isPaid, - if (paymentExpiration != null) - 'payment_expiration': paymentExpiration!.toIso8601String(), - 'indirizzo': indirizzo, - 'cap': cap, - 'comune': comune, - 'provincia': provincia, - }; - } -} diff --git a/lib/features/store/ui/create_store_screen.dart b/lib/features/store/ui/create_store_screen.dart deleted file mode 100644 index c57516a..0000000 --- a/lib/features/store/ui/create_store_screen.dart +++ /dev/null @@ -1,245 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/features/store/bloc/store_bloc.dart'; -import 'package:flux/features/store/models/store_model.dart'; -import 'package:flux/core/blocs/session/session_bloc.dart'; -import 'package:flux/core/theme/theme.dart'; -import 'package:flux/core/widgets/flux_text_field.dart'; - -class CreateStoreScreen extends StatefulWidget { - const CreateStoreScreen({super.key}); - - @override - State createState() => _CreateStoreScreenState(); -} - -class _CreateStoreScreenState extends State { - final _formKey = GlobalKey(); - - final _nomeController = TextEditingController(); - final _indirizzoController = TextEditingController(); - final _capController = TextEditingController(); - final _comuneController = TextEditingController(); - final _provinciaController = TextEditingController(); - - @override - void dispose() { - _nomeController.dispose(); - _indirizzoController.dispose(); - _capController.dispose(); - _comuneController.dispose(); - _provinciaController.dispose(); - super.dispose(); - } - - /// Funzione magica per copiare i dati dall'azienda salvata in GetIt - void _useCompanyAddress() { - final company = context.read().state.company; - if (company != null) { - setState(() { - _indirizzoController.text = company.indirizzo; - _capController.text = company.cap; - _comuneController.text = - company.citta; // Nel DB company è 'citta', store è 'comune' - _provinciaController.text = company.provincia; - // Suggeriamo anche un nome se vuoto - if (_nomeController.text.isEmpty) { - _nomeController.text = '${company.ragioneSociale} - Sede'; - } - }); - - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Dati indirizzo copiati dalla sede legale'), - ), - ); - } - } - - void _onSave() { - if (_formKey.currentState!.validate()) { - final company = context.read().state.company; - - if (company == null) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Errore: Azienda non trovata')), - ); - return; - } - - final store = StoreModel( - nome: _nomeController.text.trim(), - companyId: company.id, - indirizzo: _indirizzoController.text.trim(), - cap: _capController.text.trim(), - comune: _comuneController.text.trim(), - provincia: _provinciaController.text.trim().toUpperCase(), - ); - - context.read().add(CreateStoreRequested(store: store)); - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text('Il tuo primo Negozio')), - body: BlocConsumer( - listener: (context, state) { - if (state.status == StoreStatus.success) { - context.read().add(AppStarted()); - } - if (state.status == StoreStatus.failure) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(state.errorMessage ?? 'Errore salvataggio'), - ), - ); - } - }, - builder: (context, state) { - return SafeArea( - child: SingleChildScrollView( - padding: const EdgeInsets.all(24.0), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildHeader(context), - const SizedBox(height: 32), - - // Nome del Negozio (Icona obbligatoria) - FluxTextField( - label: 'Nome del Negozio', - icon: Icons.storefront_rounded, - controller: _nomeController, - ), - - const SizedBox(height: 24), - - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _SectionTitle(title: 'LOCALIZZAZIONE'), - TextButton.icon( - onPressed: _useCompanyAddress, - icon: const Icon(Icons.copy_rounded, size: 16), - label: const Text( - 'Copia da Azienda', - style: TextStyle(fontSize: 12), - ), - ), - ], - ), - const SizedBox(height: 12), - - // Indirizzo (Icona obbligatoria) - FluxTextField( - label: 'Indirizzo e n. civico', - icon: Icons.map_outlined, - controller: _indirizzoController, - ), - const SizedBox(height: 16), - - // RIGA TRIPLA: Comune, CAP, PR - Row( - crossAxisAlignment: CrossAxisAlignment - .start, // Allinea in alto in caso di errori - children: [ - Expanded( - flex: 3, - child: FluxTextField( - label: 'Comune', - icon: Icons.location_city_rounded, // Icona aggiunta - controller: _comuneController, - ), - ), - const SizedBox(width: 12), - Expanded( - flex: 2, - child: FluxTextField( - label: 'CAP', - icon: Icons.post_add_rounded, // Icona aggiunta - controller: _capController, - keyboardType: TextInputType.number, - ), - ), - const SizedBox(width: 12), - Expanded( - flex: 2, // Aumentato leggermente per ospitare l'icona - child: FluxTextField( - label: 'PR', - icon: Icons.explore_outlined, // Icona aggiunta - controller: _provinciaController, - ), - ), - ], - ), - - const SizedBox(height: 48), - - _buildSubmitButton(context, state), - ], - ), - ), - ), - ); - }, - ), - ); - } - - Widget _buildSubmitButton(BuildContext context, StoreState state) { - return SizedBox( - width: double.infinity, - height: 56, - child: ElevatedButton( - onPressed: state.status == StoreStatus.loading ? null : _onSave, - child: state.status == StoreStatus.loading - ? const CircularProgressIndicator(color: Colors.white) - : const Text('ATTIVA NEGOZIO'), - ), - ); - } - - Widget _buildHeader(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon(Icons.add_location_alt_rounded, color: context.accent, size: 48), - const SizedBox(height: 16), - Text( - 'Dove si trova il tuo store?', - style: Theme.of(context).textTheme.headlineMedium?.copyWith( - fontWeight: FontWeight.bold, - color: context.primaryText, - ), - ), - const SizedBox(height: 8), - Text( - 'Configura il primo punto vendita per iniziare a gestire i tuoi clienti e le operazioni.', - style: TextStyle(color: context.secondaryText, fontSize: 15), - ), - ], - ); - } -} - -class _SectionTitle extends StatelessWidget { - final String title; - const _SectionTitle({required this.title}); - - @override - Widget build(BuildContext context) { - return Text( - title, - style: TextStyle( - color: context.accent, - fontWeight: FontWeight.w800, - letterSpacing: 1.2, - fontSize: 12, - ), - ); - } -} diff --git a/lib/features/store/ui/stores_screen.dart b/lib/features/store/ui/stores_screen.dart deleted file mode 100644 index 2926314..0000000 --- a/lib/features/store/ui/stores_screen.dart +++ /dev/null @@ -1,297 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/core/blocs/session/session_bloc.dart'; -import 'package:flux/core/theme/theme.dart'; -import 'package:flux/core/widgets/flux_text_field.dart'; -import 'package:flux/features/staff/blocs/staff_cubit.dart'; -import 'package:flux/features/staff/models/staff_member_model.dart'; -import 'package:flux/features/store/bloc/store_bloc.dart'; -import 'package:flux/features/store/models/store_model.dart'; - -class StoresScreen extends StatefulWidget { - const StoresScreen({super.key}); - - @override - State createState() => _StoresScreenState(); -} - -class _StoresScreenState extends State { - @override - void initState() { - super.initState(); - // Carichiamo i negozi e anche lo staff (per poterlo assegnare) - context.read().add(LoadStoresRequested()); - context.read().loadAllStaff(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text("I Tuoi Negozi")), - body: BlocBuilder( - builder: (context, state) { - if (state.status == StoreStatus.loading) { - return const Center(child: CircularProgressIndicator()); - } - - return ListView.builder( - padding: const EdgeInsets.all(16), - itemCount: state.stores.length, - itemBuilder: (context, index) { - final store = state.stores[index]; - return _buildStoreCard(store, state.staffByStore); - }, - ); - }, - ), - floatingActionButton: FloatingActionButton.extended( - onPressed: () => _openStoreForm(context), - label: const Text("Nuovo Negozio"), - icon: const Icon(Icons.store), - ), - ); - } - - Widget _buildStoreCard( - StoreModel store, - Map> map, - ) { - return Card( - margin: const EdgeInsets.only(bottom: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - side: BorderSide( - color: store.isActive - ? Colors.transparent - : Colors.red.withValues(alpha: 0.3), - ), - ), - child: Column( - children: [ - ListTile( - leading: Icon( - Icons.storefront, - color: store.isActive ? context.accent : Colors.grey, - ), - title: Text( - store.nome, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - subtitle: Text( - "${store.comune} (${store.provincia}) - ${store.indirizzo}", - ), - trailing: Switch( - value: store.isActive, - onChanged: (val) { - // context.read().add(ToggleStoreStatus(store.id, val)); - }, - ), - ), - const Divider(height: 1), - Padding( - padding: const EdgeInsets.all(12), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - // Mostra quanti dipendenti ci sono (usando lo StaffCubit) - ActionChip( - avatar: const Icon(Icons.people, size: 16), - label: Text("${map[store]?.length ?? 0} Dipendenti"), - onPressed: () => _manageStoreStaff(store), - ), - TextButton.icon( - onPressed: () => _openStoreForm(context, store: store), - icon: const Icon(Icons.edit, size: 18), - label: const Text("Modifica"), - ), - ], - ), - ), - ], - ), - ); - } - - void _openStoreForm(BuildContext context, {StoreModel? store}) { - final nomeController = TextEditingController(text: store?.nome); - final indirizzoController = TextEditingController(text: store?.indirizzo); - final capController = TextEditingController(text: store?.cap); - final comuneController = TextEditingController(text: store?.comune); - final provinciaController = TextEditingController(text: store?.provincia); - - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (context) => Container( - decoration: BoxDecoration( - color: Theme.of(context).scaffoldBackgroundColor, - borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), - ), - padding: EdgeInsets.only( - top: 24, - left: 24, - right: 24, - bottom: MediaQuery.of(context).viewInsets.bottom + 24, - ), - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - store == null ? "Nuovo Punto Vendita" : "Modifica Negozio", - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 24), - - // --- DATI PRINCIPALI --- - FluxTextField( - controller: nomeController, - label: "Nome Negozio (es. Flux Milano)", - icon: Icons.storefront_rounded, - keyboardType: TextInputType.text, - ), - const SizedBox(height: 16), - FluxTextField( - controller: indirizzoController, - label: "Indirizzo", - icon: Icons.map_outlined, - keyboardType: TextInputType.streetAddress, - ), - const SizedBox(height: 16), - - // --- CAP, COMUNE, PROVINCIA (In riga) --- - Row( - children: [ - Expanded( - flex: 2, - child: FluxTextField( - controller: capController, - label: "CAP", - icon: Icons.local_post_office_outlined, - keyboardType: TextInputType.number, - maxLength: 5, - ), - ), - const SizedBox(width: 8), - Expanded( - flex: 4, - child: FluxTextField( - controller: comuneController, - label: "Comune", - icon: Icons.location_city_rounded, - keyboardType: TextInputType.text, - ), - ), - const SizedBox(width: 8), - Expanded( - flex: 2, - child: FluxTextField( - controller: provinciaController, - label: "Prov", - icon: Icons.explore_outlined, - keyboardType: TextInputType.text, - maxLength: 2, - onChanged: (val) => provinciaController.text = val - .toUpperCase(), // Rimpiazzo automatico in maiuscolo - // Qui potresti aggiungere un rimpiazzo automatico in maiuscolo - ), - ), - ], - ), - - const SizedBox(height: 32), - - // --- TASTO SALVA --- - SizedBox( - width: double.infinity, - height: 50, - child: ElevatedButton( - onPressed: () { - if (nomeController.text.isEmpty) return; - - final storeData = StoreModel( - id: store?.id, // Se nullo, Supabase ne crea uno nuovo - nome: nomeController.text, - indirizzo: indirizzoController.text, - cap: capController.text, - comune: comuneController.text, - provincia: provinciaController.text, - companyId: context - .read() - .state - .company! - .id, // Recuperiamo la companyId - isActive: store?.isActive ?? true, - isPaid: store?.isPaid ?? false, - paymentExpiration: store?.paymentExpiration, - ); - - // Chiamata al Bloc per il salvataggio - context.read().add( - CreateStoreRequested(store: storeData), - ); - - Navigator.pop(context); - }, - child: Text(store == null ? "CREA NEGOZIO" : "AGGIORNA DATI"), - ), - ), - ], - ), - ), - ), - ); - } - - void _manageStoreStaff(StoreModel store) { - showModalBottomSheet( - context: context, - isScrollControlled: true, - builder: (context) => BlocBuilder( - builder: (context, state) { - return Container( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text("Personale di ${store.nome}", style: context.titleLarge), - const SizedBox(height: 16), - // Lista di TUTTO lo staff dell'azienda con checkbox - ...state.allStaff.map((person) { - final bool isAssigned = - state.staffByStore[store.id!]?.any( - (s) => s.id == person.id, - ) ?? - false; - - return CheckboxListTile( - title: Text(person.name), - value: isAssigned, - onChanged: (selected) { - if (selected == true) { - context.read().assignMemberToStore( - person.id!, - store.id!, - ); - } else { - context.read().removeMemberFromStore( - person.id!, - store.id!, - ); - } - }, - ); - }), - ], - ), - ); - }, - ), - ); - } -} diff --git a/lib/main.dart b/lib/main.dart index 9ed3399..fa4d3c4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -14,7 +14,7 @@ import 'package:flux/features/master_data/products/blocs/product_cubit.dart'; import 'package:flux/features/master_data/products/data/product_repository.dart'; import 'package:flux/features/master_data/staff/blocs/staff_cubit.dart'; import 'package:flux/features/master_data/staff/data/staff_repository.dart'; -import 'package:flux/features/master_data/store/bloc/store_bloc.dart'; +import 'package:flux/features/master_data/store/bloc/store_cubit.dart'; import 'package:flux/features/master_data/store/data/store_repository.dart'; import 'package:flux/features/settings/settings.dart'; import 'package:get_it/get_it.dart'; @@ -84,10 +84,8 @@ class _FluxAppState extends State { providers: [ BlocProvider(create: (_) => AuthBloc()), BlocProvider(create: (_) => CompanyBloc()), - BlocProvider( - create: (_) => - StoreBloc(context.read()) - ..add(LoadStoresRequested()), + BlocProvider( + create: (_) => StoreCubit(context.read())..loadStores(), ), BlocProvider(create: (_) => CustomerBloc()), BlocProvider(