Refactor StoreBloc to Cubit and Fix Staff Assignment UI (#1)

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 <marco@catelli.it>
Co-committed-by: Mark M2 Macbook <marco@catelli.it>
This commit is contained in:
2026-04-15 10:05:07 +02:00
committed by brontomark
parent 7f8c0d642a
commit 753b5489b6
21 changed files with 170 additions and 1700 deletions

View File

@@ -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<StoreEvent, StoreState> {
final StoreRepository _repository = GetIt.I<StoreRepository>();
final SessionBloc _sessionBloc;
StoreBloc(this._sessionBloc) : super(const StoreState(stores: [])) {
on<CreateStoreRequested>(_onCreateStore);
on<LoadStoresRequested>(_onLoadStores);
}
Future<void> _onCreateStore(
CreateStoreRequested event,
Emitter<StoreState> 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<void> _onLoadStores(
LoadStoresRequested event,
Emitter<StoreState> 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()),
);
}
}
}

View File

@@ -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<StoreState> {
final StoreRepository _repository = GetIt.I<StoreRepository>();
final StaffRepository _staffRepository = GetIt.I<StaffRepository>();
final SessionBloc _sessionBloc;
StoreCubit(this._sessionBloc) : super(const StoreState(stores: []));
Future<void> 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<void> loadStores() async {
emit(state.copyWith(status: StoreStatus.loading));
try {
final stores = await _repository.getStoresByCompany(
_sessionBloc.state.company!.id,
);
final Map<String, List<StaffMemberModel>> 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<void> 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<String, List<StaffMemberModel>>.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<void> removeStaffFromStore(String staffId, String storeId) async {
try {
await _staffRepository.removeFromStore(staffId, storeId);
final updatedStaff = await _staffRepository.getStaffMembersInStore(
storeId,
);
final newMap = Map<String, List<StaffMemberModel>>.from(
state.staffByStore,
);
newMap[storeId] = updatedStaff;
emit(state.copyWith(staffByStore: newMap));
} catch (e) {
emit(
state.copyWith(
status: StoreStatus.failure,
errorMessage: "Errore nella rimozione: $e",
),
);
}
}
}

View File

@@ -1,18 +0,0 @@
part of 'store_bloc.dart';
abstract class StoreEvent extends Equatable {
const StoreEvent();
@override
List<Object?> get props => [];
}
class CreateStoreRequested extends StoreEvent {
final StoreModel store;
const CreateStoreRequested({required this.store});
@override
List<Object?> get props => [store];
}
class LoadStoresRequested extends StoreEvent {}

View File

@@ -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<StoreModel> stores;
final Map<String, List<StaffMemberModel>> 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<StoreModel>? stores,
Map<String, List<StaffMemberModel>>? 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<Object?> get props => [status, store, errorMessage, stores];
List<Object?> get props => [
status,
store,
errorMessage,
stores,
staffByStore,
];
}

View File

@@ -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<CreateStoreScreen> {
provincia: _provinciaController.text.trim().toUpperCase(),
);
context.read<StoreBloc>().add(CreateStoreRequested(store: store));
context.read<StoreCubit>().createStore(store);
}
}
@@ -84,7 +84,7 @@ class _CreateStoreScreenState extends State<CreateStoreScreen> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Il tuo primo Negozio')),
body: BlocConsumer<StoreBloc, StoreState>(
body: BlocConsumer<StoreCubit, StoreState>(
listener: (context, state) {
if (state.status == StoreStatus.success) {
context.read<SessionBloc>().add(AppStarted());

View File

@@ -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<StoresScreen> {
void initState() {
super.initState();
// Carichiamo i negozi e anche lo staff (per poterlo assegnare)
context.read<StoreBloc>().add(LoadStoresRequested());
context.read<StoreCubit>().loadStores();
context.read<StaffCubit>().loadAllStaff();
}
@@ -27,7 +27,7 @@ class _StoresScreenState extends State<StoresScreen> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("I Tuoi Negozi")),
body: BlocBuilder<StoreBloc, StoreState>(
body: BlocBuilder<StoreCubit, StoreState>(
builder: (context, state) {
if (state.status == StoreStatus.loading) {
return const Center(child: CircularProgressIndicator());
@@ -90,10 +90,10 @@ class _StoresScreenState extends State<StoresScreen> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Mostra quanti dipendenti ci sono (usando lo StaffCubit)
BlocBuilder<StaffCubit, StaffState>(
builder: (context, staffState) {
BlocBuilder<StoreCubit, StoreState>(
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<StoresScreen> {
context: context,
isScrollControlled: true,
builder: (context) => BlocBuilder<StaffCubit, StaffState>(
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<StoreCubit, StoreState>(
// 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<StaffCubit>().assignMemberToStore(
person.id!,
store.id!,
);
} else {
context.read<StaffCubit>().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<StoreCubit>().assignStaffToStore(
store.id!,
person.id!,
);
} else {
context.read<StoreCubit>().removeStaffFromStore(
person.id!,
store.id!,
);
}
},
);
}),
],
),
);
},
);
},
),
@@ -279,9 +289,7 @@ class _StoresScreenState extends State<StoresScreen> {
);
// Chiamata al Bloc per il salvataggio
context.read<StoreBloc>().add(
CreateStoreRequested(store: storeData),
);
context.read<StoreCubit>().createStore(storeData);
Navigator.pop(context);
},