Refactor Staff and Store models to use fromMap method; enhance StoreCubit with provider management functionality

This commit is contained in:
2026-04-17 12:40:58 +02:00
parent 22a4f1dac4
commit 08a521c21c
6 changed files with 176 additions and 38 deletions

View File

@@ -16,17 +16,17 @@ class StaffRepository {
.eq('company_id', companyId) .eq('company_id', companyId)
.order('name', ascending: true); .order('name', ascending: true);
return (response as List).map((s) => StaffMemberModel.fromJson(s)).toList(); return (response as List).map((s) => StaffMemberModel.fromMap(s)).toList();
} }
Future<StaffMemberModel> saveStaffMember(StaffMemberModel member) async { Future<StaffMemberModel> saveStaffMember(StaffMemberModel member) async {
final response = await _supabase final response = await _supabase
.from('staff_member') .from('staff_member')
.upsert(member.toJson()) .upsert(member.toMap())
.select() .select()
.single(); .single();
return StaffMemberModel.fromJson(response); return StaffMemberModel.fromMap(response);
} }
// --- LOGICA DI GIUNZIONE (Staff <-> Store) --- // --- LOGICA DI GIUNZIONE (Staff <-> Store) ---
@@ -42,7 +42,7 @@ class StaffRepository {
.eq('store_id', storeId); .eq('store_id', storeId);
return (response as List) return (response as List)
.map((item) => StaffMemberModel.fromJson(item['staff_member'])) .map((item) => StaffMemberModel.fromMap(item['staff_member']))
.toList(); .toList();
} }

View File

@@ -18,20 +18,20 @@ class StaffMemberModel extends Equatable {
required this.companyId, required this.companyId,
}); });
factory StaffMemberModel.fromJson(Map<String, dynamic> json) { factory StaffMemberModel.fromMap(Map<String, dynamic> map) {
return StaffMemberModel( return StaffMemberModel(
id: json['id'], id: map['id'],
// Applichiamo il tuo myFormat per visualizzare i nomi correttamente // Applichiamo il tuo myFormat per visualizzare i nomi correttamente
name: (json['name'] as String).myFormat(), name: (map['name'] as String).myFormat(),
// L'email la teniamo lowercase per standard tecnico // L'email la teniamo lowercase per standard tecnico
email: (json['email'] as String? ?? '').toLowerCase().trim(), email: (map['email'] as String? ?? '').toLowerCase().trim(),
phone: (json['phone'] as String? ?? '').trim(), phone: (map['phone'] as String? ?? '').trim(),
isActive: json['is_active'] ?? true, isActive: map['is_active'] ?? true,
companyId: json['company_id'], companyId: map['company_id'],
); );
} }
Map<String, dynamic> toJson() { Map<String, dynamic> toMap() {
return { return {
if (id != null) 'id': id, if (id != null) 'id': id,
'name': name.toLowerCase().trim(), // Salviamo pulito per le query 'name': name.toLowerCase().trim(), // Salviamo pulito per le query

View File

@@ -54,6 +54,58 @@ class StoreCubit extends Cubit<StoreState> {
} }
} }
Future<void> assignProviderToStore(String storeId, String providerId) async {
try {
await _repository.associateProviderToStore(
providerId: providerId,
storeId: storeId,
);
// Dopo l'associazione, potresti voler ricaricare i provider per quel negozio
final updatedProviders = await _repository.fetchProvidersForStore(
storeId,
);
final newMap = Map<String, List<ProviderModel>>.from(
state.providersByStore,
);
newMap[storeId] = updatedProviders;
emit(state.copyWith(providersByStore: newMap));
} catch (e) {
emit(
state.copyWith(
status: StoreStatus.failure,
errorMessage: "Errore nell'associazione: $e",
),
);
}
}
Future<void> removeProviderFromStore(
String storeId,
String providerId,
) async {
try {
await _repository.removeProviderFromStore(
providerId: providerId,
storeId: storeId,
);
final updatedProviders = await _repository.fetchProvidersForStore(
storeId,
);
final newMap = Map<String, List<ProviderModel>>.from(
state.providersByStore,
);
newMap[storeId] = updatedProviders;
emit(state.copyWith(providersByStore: newMap));
} catch (e) {
emit(
state.copyWith(
status: StoreStatus.failure,
errorMessage: "Errore nella rimozione: $e",
),
);
}
}
Future<void> assignStaffToStore(String storeId, String staffId) async { Future<void> assignStaffToStore(String storeId, String staffId) async {
try { try {
await _staffRepository.assignToStore(staffId, storeId); await _staffRepository.assignToStore(staffId, storeId);

View File

@@ -24,8 +24,16 @@ class StoreRepository {
.from('store') .from('store')
.select(''' .select('''
*, *,
providers_count:providers_in_stores(count), associated_providers:providers_in_stores (
staff_members_count:staff_in_stores(count) provider (
*
)
)
associated_staff:staff_in_stores (
staff_member (
*
)
)
''') ''')
.eq('company_id', companyId) .eq('company_id', companyId)
.order('nome'); .order('nome');

View File

@@ -1,4 +1,6 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flux/features/master_data/providers/models/provider_model.dart';
import 'package:flux/features/master_data/staff/models/staff_member_model.dart';
class StoreModel extends Equatable { class StoreModel extends Equatable {
final String? id; final String? id;
@@ -11,9 +13,9 @@ class StoreModel extends Equatable {
final String cap; final String cap;
final String comune; final String comune;
final String provincia; final String provincia;
final int providersCount; // Numero di provider associati, utile per la lista final List<ProviderModel> associatedProviders; // Provider associati
final int final List<StaffMemberModel>
staffMembersCount; // Numero di membri dello staff associati, utile per la lista associatedStaffMembers; // Membri dello staff associati
const StoreModel({ const StoreModel({
this.id, this.id,
@@ -26,8 +28,8 @@ class StoreModel extends Equatable {
required this.cap, required this.cap,
required this.comune, required this.comune,
required this.provincia, required this.provincia,
this.providersCount = 0, // Default a 0 se non specificato this.associatedProviders = const [],
this.staffMembersCount = 0, // Default a 0 se non specificato this.associatedStaffMembers = const [],
}); });
// Fondamentale per Equatable: definisce quali proprietà determinano l'uguaglianza // Fondamentale per Equatable: definisce quali proprietà determinano l'uguaglianza
@@ -43,8 +45,8 @@ class StoreModel extends Equatable {
cap, cap,
comune, comune,
provincia, provincia,
providersCount, associatedProviders,
staffMembersCount, associatedStaffMembers,
]; ];
// Il mitico copyWith per creare nuove istanze modificando solo ciò che serve // Il mitico copyWith per creare nuove istanze modificando solo ciò che serve
@@ -59,8 +61,8 @@ class StoreModel extends Equatable {
String? cap, String? cap,
String? comune, String? comune,
String? provincia, String? provincia,
int? providersCount, List<ProviderModel>? associatedProviders,
int? staffMembersCount, List<StaffMemberModel>? associatedStaffMembers,
}) { }) {
return StoreModel( return StoreModel(
id: id ?? this.id, id: id ?? this.id,
@@ -73,12 +75,36 @@ class StoreModel extends Equatable {
cap: cap ?? this.cap, cap: cap ?? this.cap,
comune: comune ?? this.comune, comune: comune ?? this.comune,
provincia: provincia ?? this.provincia, provincia: provincia ?? this.provincia,
providersCount: providersCount ?? this.providersCount, associatedProviders: associatedProviders ?? this.associatedProviders,
staffMembersCount: staffMembersCount ?? this.staffMembersCount, associatedStaffMembers:
associatedStaffMembers ?? this.associatedStaffMembers,
); );
} }
factory StoreModel.fromMap(Map<String, dynamic> map) { factory StoreModel.fromMap(Map<String, dynamic> map) {
final providersPivotList = map['associated_providers'] as List?;
List<ProviderModel> providers = [];
if (providersPivotList != null) {
providers = providersPivotList
.where((item) => item['provider'] != null) // Sicurezza
.map(
(item) =>
ProviderModel.fromMap(item['provider'] as Map<String, dynamic>),
)
.toList();
}
final staffPivotList = map['associated_staff'] as List?;
List<StaffMemberModel> staffMembers = [];
if (staffPivotList != null) {
staffMembers = staffPivotList
.where((item) => item['staff_member'] != null) // Sicurezza
.map(
(item) => StaffMemberModel.fromMap(
item['staff_member'] as Map<String, dynamic>,
),
)
.toList();
}
return StoreModel( return StoreModel(
id: map['id'] as String, id: map['id'] as String,
nome: map['nome'], nome: map['nome'],
@@ -92,15 +118,8 @@ class StoreModel extends Equatable {
cap: map['cap'], cap: map['cap'],
comune: map['comune'], comune: map['comune'],
provincia: map['provincia'], provincia: map['provincia'],
providersCount: associatedProviders: providers,
map['providers_count'] != null && map['providers_count'].isNotEmpty associatedStaffMembers: staffMembers,
? map['providers_count'][0]['count'] as int
: 0,
staffMembersCount:
map['staff_members_count'] != null &&
map['staff_members_count'].isNotEmpty
? map['staff_members_count'][0]['count'] as int
: 0,
); );
} }

View File

@@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/blocs/session/session_bloc.dart'; import 'package:flux/core/blocs/session/session_bloc.dart';
import 'package:flux/core/theme/theme.dart'; import 'package:flux/core/theme/theme.dart';
import 'package:flux/core/widgets/flux_text_field.dart'; import 'package:flux/core/widgets/flux_text_field.dart';
import 'package:flux/features/master_data/providers/blocs/provider_cubit.dart';
import 'package:flux/features/master_data/staff/blocs/staff_cubit.dart'; import 'package:flux/features/master_data/staff/blocs/staff_cubit.dart';
import 'package:flux/features/master_data/store/bloc/store_cubit.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/features/master_data/store/models/store_model.dart';
@@ -94,13 +95,26 @@ class _StoresScreenState extends State<StoresScreen> {
builder: (context, storeState) { builder: (context, storeState) {
final staffCount = final staffCount =
storeState.staffByStore[store.id]?.length ?? 0; storeState.staffByStore[store.id]?.length ?? 0;
return ActionChip( return Row(
avatar: const Icon(Icons.people, size: 16), children: [
label: Text("$staffCount Dipendenti"), ActionChip(
onPressed: () => _manageStoreStaff(store), avatar: const Icon(Icons.people, size: 16),
label: Text("$staffCount Dipendenti"),
onPressed: () => _manageStoreStaff(store),
),
const SizedBox(width: 16),
ActionChip(
avatar: const Icon(Icons.handshake, size: 16),
label: Text("${store.providersCount} Providers"),
onPressed: () {
// Potresti voler navigare alla lista dei provider associati a questo store
},
),
],
); );
}, },
), ),
const SizedBox(width: 16),
TextButton.icon( TextButton.icon(
onPressed: () => _openStoreForm(context, store: store), onPressed: () => _openStoreForm(context, store: store),
icon: const Icon(Icons.edit, size: 18), icon: const Icon(Icons.edit, size: 18),
@@ -171,6 +185,51 @@ class _StoresScreenState extends State<StoresScreen> {
); );
} }
void _manageStoreProviders(StoreModel store) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => BlocBuilder<ProvidersCubit, ProvidersState>(
builder: (context, state) {
// Qui dentro hai già tutta la lista dei provider e quelli associati a questo store
// Puoi fare una UI simile a quella dello staff, con una checkbox per ogni provider
return Container(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text("Providers di ${store.nome}", style: context.titleLarge),
const SizedBox(height: 16),
...state.allProviders.map((provider) {
final isAssociated = state.associatedIds.contains(
provider.id,
);
return CheckboxListTile(
title: Text(provider.nome),
value: isAssociated,
onChanged: (selected) {
if (selected == true) {
context.read<ProvidersCubit>().assignProviderToStore(
store.id!,
provider.id!,
);
} else {
context.read<ProvidersCubit>().removeProviderFromStore(
provider.id!,
store.id!,
);
}
},
);
}),
],
),
);
},
),
);
}
void _openStoreForm(BuildContext context, {StoreModel? store}) { void _openStoreForm(BuildContext context, {StoreModel? store}) {
final nomeController = TextEditingController(text: store?.nome); final nomeController = TextEditingController(text: store?.nome);
final indirizzoController = TextEditingController(text: store?.indirizzo); final indirizzoController = TextEditingController(text: store?.indirizzo);