default provider
Some checks failed
Build and Release FLUX (Multi-Platform) / build-android (push) Successful in 1m59s
Build and Release FLUX (Multi-Platform) / build-web (push) Successful in 1m22s
Build and Release FLUX (Multi-Platform) / build-windows (push) Has been cancelled

This commit is contained in:
2026-06-02 13:12:21 +02:00
parent a51ac8fe7f
commit 3210b4fcfa
12 changed files with 435 additions and 170 deletions

View File

@@ -0,0 +1,28 @@
import 'package:flux/features/master_data/providers/models/provider_model.dart';
import 'package:flux/features/master_data/providers/models/provider_role.dart';
extension ProviderCompatibility on ProviderModel {
bool supportsOperation(String operationType) {
if (operationType == 'Altro') return true;
switch (operationType) {
case 'AL' || 'MNP':
return roles.contains(ProviderRole.mobile);
case 'NIP' || 'FWA':
return roles.contains(ProviderRole.landline);
case 'UNICA':
return roles.contains(ProviderRole.landline) ||
roles.contains(ProviderRole.mobile);
case 'Energy':
return roles.contains(ProviderRole.energy);
case 'Fin':
return roles.contains(ProviderRole.financing);
case 'Entertainment':
return roles.contains(ProviderRole.entertainment);
case 'TELEPASS':
return roles.contains(ProviderRole.telepass);
default:
return true;
}
}
}

View File

@@ -17,14 +17,18 @@ class StoreCubit extends Cubit<StoreState> {
StoreCubit() : super(const StoreState(stores: []));
Future<void> createStore(final StoreModel store) async {
Future<void> saveStore(final StoreModel store) async {
emit(state.copyWith(status: StoreStatus.loading));
try {
await _repository.createStore(store);
emit(state.copyWith(status: StoreStatus.success));
final savedStore = await _repository.saveStore(store);
emit(state.copyWith(status: StoreStatus.success, savedStore: savedStore));
} catch (e) {
emit(
state.copyWith(status: StoreStatus.failure, errorMessage: e.toString()),
state.copyWith(
status: StoreStatus.failure,
errorMessage: e.toString(),
savedStore: null,
),
);
}
}
@@ -70,6 +74,7 @@ class StoreCubit extends Cubit<StoreState> {
state.copyWith(
status: StoreStatus.failure,
errorMessage: "Errore nel salvataggio dei provider: $e",
savedStore: null,
),
);
}
@@ -90,6 +95,7 @@ class StoreCubit extends Cubit<StoreState> {
state.copyWith(
status: StoreStatus.failure,
errorMessage: "Errore nel salvataggio dello staff: $e",
savedStore: null,
),
);
}
@@ -110,6 +116,7 @@ class StoreCubit extends Cubit<StoreState> {
state.copyWith(
status: StoreStatus.failure,
errorMessage: "Errore nell'associazione: $e",
savedStore: null,
),
);
}
@@ -130,6 +137,7 @@ class StoreCubit extends Cubit<StoreState> {
state.copyWith(
status: StoreStatus.failure,
errorMessage: "Errore nella rimozione: $e",
savedStore: null,
),
);
}
@@ -142,7 +150,11 @@ class StoreCubit extends Cubit<StoreState> {
loadStores();
} catch (e) {
emit(
state.copyWith(status: StoreStatus.failure, errorMessage: e.toString()),
state.copyWith(
status: StoreStatus.failure,
errorMessage: e.toString(),
savedStore: null,
),
);
}
}
@@ -157,6 +169,7 @@ class StoreCubit extends Cubit<StoreState> {
state.copyWith(
status: StoreStatus.failure,
errorMessage: "Errore nella rimozione: $e",
savedStore: null,
),
);
}

View File

@@ -7,6 +7,8 @@ class StoreState extends Equatable {
final StoreModel? store;
final String? errorMessage;
final List<StoreModel> stores;
final StoreModel?
savedStore; // Per tenere traccia del negozio appena salvato (utile per aggiornare la sessione)
final Map<String, List<StaffMemberModel>> staffByStore;
const StoreState({
@@ -14,6 +16,7 @@ class StoreState extends Equatable {
this.store,
this.errorMessage,
required this.stores,
this.savedStore,
this.staffByStore = const {},
});
@@ -22,6 +25,7 @@ class StoreState extends Equatable {
StoreModel? store,
String? errorMessage,
List<StoreModel>? stores,
StoreModel? savedStore,
Map<String, List<StaffMemberModel>>? staffByStore,
}) {
return StoreState(
@@ -29,6 +33,7 @@ class StoreState extends Equatable {
store: store ?? this.store,
errorMessage: errorMessage ?? this.errorMessage,
stores: stores ?? this.stores,
savedStore: savedStore ?? this.savedStore,
staffByStore: staffByStore ?? this.staffByStore,
);
}
@@ -39,6 +44,7 @@ class StoreState extends Equatable {
store,
errorMessage,
stores,
savedStore,
staffByStore,
];
}

View File

@@ -9,7 +9,7 @@ class StoreRepository {
final SupabaseClient _supabase = GetIt.I.get<SupabaseClient>();
/// Crea un nuovo negozio associato alla compagnia dell'utente
Future<void> createStore(StoreModel store) async {
/* Future<void> createStore(StoreModel store) async {
try {
await _supabase.from(Tables.stores).insert(store.toMap());
} on PostgrestException catch (e) {
@@ -18,7 +18,7 @@ class StoreRepository {
} catch (e) {
throw 'Errore imprevisto durante la creazione del negozio: $e';
}
}
} */
Future<StoreModel> saveStore(StoreModel store) async {
try {

View File

@@ -16,6 +16,7 @@ class StoreModel extends Equatable {
final List<ProviderModel> associatedProviders; // Provider associati
final List<StaffMemberModel>
associatedStaffMembers; // Membri dello staff associati
final String? defaultProviderId; // ID del provider di default (opzionale)
const StoreModel({
this.id,
@@ -30,6 +31,7 @@ class StoreModel extends Equatable {
required this.province,
this.associatedProviders = const [],
this.associatedStaffMembers = const [],
this.defaultProviderId,
});
// Fondamentale per Equatable: definisce quali proprietà determinano l'uguaglianza
@@ -47,6 +49,7 @@ class StoreModel extends Equatable {
province,
associatedProviders,
associatedStaffMembers,
defaultProviderId,
];
// Il mitico copyWith per creare nuove istanze modificando solo ciò che serve
@@ -63,6 +66,7 @@ class StoreModel extends Equatable {
String? province,
List<ProviderModel>? associatedProviders,
List<StaffMemberModel>? associatedStaffMembers,
String? Function()? defaultProviderId,
}) {
return StoreModel(
id: id ?? this.id,
@@ -78,6 +82,9 @@ class StoreModel extends Equatable {
associatedProviders: associatedProviders ?? this.associatedProviders,
associatedStaffMembers:
associatedStaffMembers ?? this.associatedStaffMembers,
defaultProviderId: defaultProviderId != null
? defaultProviderId()
: this.defaultProviderId,
);
}
@@ -131,6 +138,7 @@ class StoreModel extends Equatable {
province: map['province'],
associatedProviders: providers,
associatedStaffMembers: staffMembers,
defaultProviderId: map['default_provider_id'] as String?,
);
}
@@ -147,6 +155,7 @@ class StoreModel extends Equatable {
'zip_code': zipCode,
'city': city,
'province': province,
'default_provider_id': defaultProviderId,
};
}
}

View File

@@ -76,7 +76,7 @@ class _CreateStoreScreenState extends State<CreateStoreScreen> {
province: _provinciaController.text.trim().toUpperCase(),
);
context.read<StoreCubit>().createStore(store);
context.read<StoreCubit>().saveStore(store);
}
}

View File

@@ -2,6 +2,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/widgets/flux_text_field.dart';
import 'package:flux/features/master_data/providers/blocs/provider_list_cubit.dart';
import 'package:flux/features/master_data/store/bloc/store_cubit.dart';
import 'package:flux/features/master_data/store/models/store_model.dart';
@@ -19,6 +20,8 @@ class _StoreFormState extends State<StoreForm> {
final capController = TextEditingController();
final comuneController = TextEditingController();
final provinciaController = TextEditingController();
String?
_selectedDefaultProviderId; // Per tenere traccia del provider di default selezionato
@override
void initState() {
@@ -29,129 +32,241 @@ class _StoreFormState extends State<StoreForm> {
capController.text = widget.store!.zipCode;
comuneController.text = widget.store!.city;
provinciaController.text = widget.store!.province;
_selectedDefaultProviderId = widget.store!.defaultProviderId;
}
context.read<ProviderListCubit>().loadProviders(
widget.store!.id!,
); // Carichiamo i gestori per la dropdown
}
@override
Widget build(BuildContext context) {
return 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(
widget.store == null ? "Nuovo Punto Vendita" : "Modifica Negozio",
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
return BlocListener<StoreCubit, StoreState>(
listener: (context, state) {
if (state.status == StoreStatus.success) {
// 1. Diciamo alla schermata di ricaricare la lista generale dei negozi (se serve)
context.read<StoreCubit>().loadStores();
// 🥷 2. IL TOCCO FINALE: Aggiorniamo la sessione globale se stiamo modificando il negozio attivo!
if (state.savedStore != null) {
context.read<SessionCubit>().updateCurrentStoreLocally(
state.savedStore!,
);
}
// 3. Chiudiamo il form
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Negozio aggiornato con successo!',
style: TextStyle(color: Colors.white),
),
backgroundColor: Colors.green,
),
const SizedBox(height: 24),
);
Navigator.pop(context);
}
// --- DATI PRINCIPALI ---
FluxTextField(
controller: nomeController,
label: "Nome Negozio (es. Flux Milano)",
icon: Icons.storefront_rounded,
keyboardType: TextInputType.name,
if (state.status == StoreStatus.failure) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.errorMessage ?? 'Errore di salvataggio'),
backgroundColor: Colors.red,
),
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.post_add_rounded,
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.name,
),
),
const SizedBox(width: 8),
Expanded(
flex: 2,
child: FluxTextField(
controller: provinciaController,
label: "Prov",
icon: Icons.explore_outlined,
keyboardType: TextInputType.name,
onChanged: (value) => value.toUpperCase(),
maxLength: 2,
),
),
],
),
const SizedBox(height: 32),
// --- TASTO SALVA ---
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: () {
if (nomeController.text.isEmpty) return;
final storeData = StoreModel(
id: widget
.store
?.id, // Se nullo, Supabase ne crea uno nuovo
name: nomeController.text,
address: indirizzoController.text,
zipCode: capController.text,
city: comuneController.text,
province: provinciaController.text,
companyId: context
.read<SessionCubit>()
.state
.company!
.id!, // Recuperiamo la companyId
isActive: widget.store?.isActive ?? true,
isPaid: widget.store?.isPaid ?? false,
paymentExpiration: widget.store?.paymentExpiration,
);
// Chiamata al Bloc per il salvataggio
context.read<StoreCubit>().createStore(storeData);
Navigator.pop(context);
},
child: Text(
widget.store == null ? "CREA NEGOZIO" : "AGGIORNA DATI",
);
}
},
child: 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(
widget.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.name,
),
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.post_add_rounded,
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.name,
),
),
const SizedBox(width: 8),
Expanded(
flex: 2,
child: FluxTextField(
controller: provinciaController,
label: "Prov",
icon: Icons.explore_outlined,
keyboardType: TextInputType.name,
onChanged: (value) => value.toUpperCase(),
maxLength: 2,
),
),
],
),
const SizedBox(height: 16),
// --- GESTORI ---
_defaultProviderDropdown(),
const SizedBox(height: 32),
// --- TASTO SALVA ---
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: () {
if (nomeController.text.isEmpty) return;
final storeData = StoreModel(
id: widget
.store
?.id, // Se nullo, Supabase ne crea uno nuovo
name: nomeController.text,
address: indirizzoController.text,
zipCode: capController.text,
city: comuneController.text,
province: provinciaController.text,
companyId: context
.read<SessionCubit>()
.state
.company!
.id!, // Recuperiamo la companyId
isActive: widget.store?.isActive ?? true,
isPaid: widget.store?.isPaid ?? false,
paymentExpiration: widget.store?.paymentExpiration,
defaultProviderId: _selectedDefaultProviderId,
);
// Chiamata al Bloc per il salvataggio
context.read<StoreCubit>().saveStore(storeData);
},
child: Text(
widget.store == null ? "CREA NEGOZIO" : "AGGIORNA DATI",
),
),
),
],
),
),
),
);
}
Widget _defaultProviderDropdown() {
return BlocBuilder<ProviderListCubit, ProviderListState>(
builder: (context, state) {
if (state.status == ProviderListStatus.loading) {
return const Center(child: CircularProgressIndicator());
}
final activeProviders = state.providers
.where((p) => p.isActive)
.toList();
// 🥷 SCENARIO ONBOARDING: La lista dei gestori è vuota
if (activeProviders.isEmpty) {
return TextFormField(
enabled: false, // Disabilitiamo il campo
decoration: InputDecoration(
labelText: 'Gestore di Default',
hintText: 'Configura prima i gestori nell\'hub anagrafiche',
hintStyle: TextStyle(color: Colors.grey[500], fontSize: 13),
prefixIcon: const Icon(Icons.star_border, color: Colors.grey),
disabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: Colors.grey[300]!),
),
fillColor: Colors.grey[50],
filled: true,
),
);
}
// SCENARIO STANDARD: Ci sono gestori censiti, mostriamo la dropdown
return DropdownButtonFormField<String?>(
initialValue: _selectedDefaultProviderId,
decoration: InputDecoration(
labelText: 'Gestore di Default (Opzionale)',
hintText: 'Seleziona se questo è un negozio monomarca',
prefixIcon: const Icon(Icons.star_border, color: Colors.amber),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
),
items: [
const DropdownMenuItem<String?>(
value: null,
child: Text(
'Nessun gestore (Multi-brand)',
style: TextStyle(fontStyle: FontStyle.italic),
),
),
...activeProviders.map((p) {
return DropdownMenuItem<String?>(
value: p.id,
child: Text(p.name),
);
}),
],
onChanged: (val) {
setState(() {
_selectedDefaultProviderId = val;
});
},
);
},
);
}
}