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

@@ -247,4 +247,13 @@ class SessionCubit extends Cubit<SessionState> {
void setIsSingleUserMode(bool isSingleUser) { void setIsSingleUserMode(bool isSingleUser) {
emit(state.copyWith(isSingleUserMode: isSingleUser)); emit(state.copyWith(isSingleUserMode: isSingleUser));
} }
void updateCurrentStoreLocally(StoreModel updatedStore) {
// Verifichiamo che l'utente stia effettivamente lavorando nel negozio appena modificato
if (state.currentStore != null &&
state.currentStore!.id == updatedStore.id) {
// Emettiamo il nuovo stato sovrascrivendo solo il negozio corrente
emit(state.copyWith(currentStore: updatedStore));
}
}
} }

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: [])); 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)); emit(state.copyWith(status: StoreStatus.loading));
try { try {
await _repository.createStore(store); final savedStore = await _repository.saveStore(store);
emit(state.copyWith(status: StoreStatus.success)); emit(state.copyWith(status: StoreStatus.success, savedStore: savedStore));
} catch (e) { } catch (e) {
emit( 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( state.copyWith(
status: StoreStatus.failure, status: StoreStatus.failure,
errorMessage: "Errore nel salvataggio dei provider: $e", errorMessage: "Errore nel salvataggio dei provider: $e",
savedStore: null,
), ),
); );
} }
@@ -90,6 +95,7 @@ class StoreCubit extends Cubit<StoreState> {
state.copyWith( state.copyWith(
status: StoreStatus.failure, status: StoreStatus.failure,
errorMessage: "Errore nel salvataggio dello staff: $e", errorMessage: "Errore nel salvataggio dello staff: $e",
savedStore: null,
), ),
); );
} }
@@ -110,6 +116,7 @@ class StoreCubit extends Cubit<StoreState> {
state.copyWith( state.copyWith(
status: StoreStatus.failure, status: StoreStatus.failure,
errorMessage: "Errore nell'associazione: $e", errorMessage: "Errore nell'associazione: $e",
savedStore: null,
), ),
); );
} }
@@ -130,6 +137,7 @@ class StoreCubit extends Cubit<StoreState> {
state.copyWith( state.copyWith(
status: StoreStatus.failure, status: StoreStatus.failure,
errorMessage: "Errore nella rimozione: $e", errorMessage: "Errore nella rimozione: $e",
savedStore: null,
), ),
); );
} }
@@ -142,7 +150,11 @@ class StoreCubit extends Cubit<StoreState> {
loadStores(); loadStores();
} catch (e) { } catch (e) {
emit( 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( state.copyWith(
status: StoreStatus.failure, status: StoreStatus.failure,
errorMessage: "Errore nella rimozione: $e", errorMessage: "Errore nella rimozione: $e",
savedStore: null,
), ),
); );
} }

View File

@@ -7,6 +7,8 @@ class StoreState extends Equatable {
final StoreModel? store; final StoreModel? store;
final String? errorMessage; final String? errorMessage;
final List<StoreModel> stores; 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; final Map<String, List<StaffMemberModel>> staffByStore;
const StoreState({ const StoreState({
@@ -14,6 +16,7 @@ class StoreState extends Equatable {
this.store, this.store,
this.errorMessage, this.errorMessage,
required this.stores, required this.stores,
this.savedStore,
this.staffByStore = const {}, this.staffByStore = const {},
}); });
@@ -22,6 +25,7 @@ class StoreState extends Equatable {
StoreModel? store, StoreModel? store,
String? errorMessage, String? errorMessage,
List<StoreModel>? stores, List<StoreModel>? stores,
StoreModel? savedStore,
Map<String, List<StaffMemberModel>>? staffByStore, Map<String, List<StaffMemberModel>>? staffByStore,
}) { }) {
return StoreState( return StoreState(
@@ -29,6 +33,7 @@ class StoreState extends Equatable {
store: store ?? this.store, store: store ?? this.store,
errorMessage: errorMessage ?? this.errorMessage, errorMessage: errorMessage ?? this.errorMessage,
stores: stores ?? this.stores, stores: stores ?? this.stores,
savedStore: savedStore ?? this.savedStore,
staffByStore: staffByStore ?? this.staffByStore, staffByStore: staffByStore ?? this.staffByStore,
); );
} }
@@ -39,6 +44,7 @@ class StoreState extends Equatable {
store, store,
errorMessage, errorMessage,
stores, stores,
savedStore,
staffByStore, staffByStore,
]; ];
} }

View File

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

View File

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

View File

@@ -76,7 +76,7 @@ class _CreateStoreScreenState extends State<CreateStoreScreen> {
province: _provinciaController.text.trim().toUpperCase(), 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:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/blocs/session/session_cubit.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_list_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';
@@ -19,6 +20,8 @@ class _StoreFormState extends State<StoreForm> {
final capController = TextEditingController(); final capController = TextEditingController();
final comuneController = TextEditingController(); final comuneController = TextEditingController();
final provinciaController = TextEditingController(); final provinciaController = TextEditingController();
String?
_selectedDefaultProviderId; // Per tenere traccia del provider di default selezionato
@override @override
void initState() { void initState() {
@@ -29,129 +32,241 @@ class _StoreFormState extends State<StoreForm> {
capController.text = widget.store!.zipCode; capController.text = widget.store!.zipCode;
comuneController.text = widget.store!.city; comuneController.text = widget.store!.city;
provinciaController.text = widget.store!.province; provinciaController.text = widget.store!.province;
_selectedDefaultProviderId = widget.store!.defaultProviderId;
} }
context.read<ProviderListCubit>().loadProviders(
widget.store!.id!,
); // Carichiamo i gestori per la dropdown
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return BlocListener<StoreCubit, StoreState>(
decoration: BoxDecoration( listener: (context, state) {
color: Theme.of(context).scaffoldBackgroundColor, if (state.status == StoreStatus.success) {
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), // 1. Diciamo alla schermata di ricaricare la lista generale dei negozi (se serve)
), context.read<StoreCubit>().loadStores();
padding: EdgeInsets.only(
top: 24, // 🥷 2. IL TOCCO FINALE: Aggiorniamo la sessione globale se stiamo modificando il negozio attivo!
left: 24, if (state.savedStore != null) {
right: 24, context.read<SessionCubit>().updateCurrentStoreLocally(
bottom: MediaQuery.of(context).viewInsets.bottom + 24, state.savedStore!,
), );
child: SingleChildScrollView( }
child: Column(
mainAxisSize: MainAxisSize.min, // 3. Chiudiamo il form
crossAxisAlignment: CrossAxisAlignment.start, ScaffoldMessenger.of(context).showSnackBar(
children: [ const SnackBar(
Text( content: Text(
widget.store == null ? "Nuovo Punto Vendita" : "Modifica Negozio", 'Negozio aggiornato con successo!',
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), style: TextStyle(color: Colors.white),
),
backgroundColor: Colors.green,
), ),
const SizedBox(height: 24), );
Navigator.pop(context);
}
// --- DATI PRINCIPALI --- if (state.status == StoreStatus.failure) {
FluxTextField( ScaffoldMessenger.of(context).showSnackBar(
controller: nomeController, SnackBar(
label: "Nome Negozio (es. Flux Milano)", content: Text(state.errorMessage ?? 'Errore di salvataggio'),
icon: Icons.storefront_rounded, backgroundColor: Colors.red,
keyboardType: TextInputType.name,
), ),
const SizedBox(height: 16), );
FluxTextField( }
controller: indirizzoController, },
label: "Indirizzo", child: Container(
icon: Icons.map_outlined, decoration: BoxDecoration(
keyboardType: TextInputType.streetAddress, color: Theme.of(context).scaffoldBackgroundColor,
), borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
const SizedBox(height: 16), ),
padding: EdgeInsets.only(
// --- CAP, COMUNE, PROVINCIA (In riga) --- top: 24,
Row( left: 24,
children: [ right: 24,
Expanded( bottom: MediaQuery.of(context).viewInsets.bottom + 24,
flex: 2, ),
child: FluxTextField( child: SingleChildScrollView(
controller: capController, child: Column(
label: "CAP", mainAxisSize: MainAxisSize.min,
icon: Icons.post_add_rounded, crossAxisAlignment: CrossAxisAlignment.start,
keyboardType: TextInputType.number, children: [
maxLength: 5, Text(
), widget.store == null
), ? "Nuovo Punto Vendita"
const SizedBox(width: 8), : "Modifica Negozio",
Expanded( style: const TextStyle(
flex: 4, fontSize: 20,
child: FluxTextField( fontWeight: FontWeight.bold,
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",
), ),
), ),
), 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;
});
},
);
},
);
}
} }

View File

@@ -2,6 +2,8 @@ import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/blocs/session/session_cubit.dart';
import 'package:flux/features/customers/models/customer_model.dart'; import 'package:flux/features/customers/models/customer_model.dart';
import 'package:flux/features/master_data/providers/models/provider_model.dart';
import 'package:flux/features/master_data/providers/models/provider_model_extensions.dart';
import 'package:flux/features/master_data/staff/models/staff_member_model.dart'; import 'package:flux/features/master_data/staff/models/staff_member_model.dart';
import 'package:flux/features/operations/data/operations_repository.dart'; import 'package:flux/features/operations/data/operations_repository.dart';
import 'package:flux/features/operations/models/operation_model.dart'; import 'package:flux/features/operations/models/operation_model.dart';
@@ -278,27 +280,126 @@ class OperationFormCubit extends Cubit<OperationFormState> {
// --- UTILS --- // --- UTILS ---
void setTypeWithSmartDefault(String type) { void updateOperationType(
String newType, {
required List<ProviderModel> allProviders,
String? defaultProviderId,
}) {
// 1. Aggiorniamo il tipo nel modello in canna
// (Presumo tu abbia un metodo copyWith o simile)
final updatedOp = state.operation.copyWith(type: newType, subtype: '');
// 2. Prepariamoci ad auto-selezionare il provider
String? newProviderId = updatedOp.providerId;
String? newProviderName = updatedOp.providerDisplayName;
// 3. LA LOGICA DI DEFAULT
if (defaultProviderId != null) {
// Troviamo il provider di default nella lista
final defaultProvider = allProviders
.where((p) => p.id == defaultProviderId)
.firstOrNull;
if (defaultProvider != null) {
// Usiamo l'extension appena creata!
if (defaultProvider.supportsOperation(newType)) {
newProviderId = defaultProvider.id;
newProviderName = defaultProvider.name;
} else {
// Se cambi tipo (es. da Mobile a Luce) e il default non lo supporta, sbianchiamo
newProviderId = null;
newProviderName = null;
}
}
}
// Emettiamo il nuovo stato
emit(
state.copyWith(
operation: updatedOp.copyWith(
providerId: newProviderId,
providerDisplayName: newProviderName,
),
),
);
}
void setTypeWithSmartDefaults({
required String newType,
required List<ProviderModel> allProviders,
String? defaultProviderId,
}) {
final currentOp = state.operation;
// -----------------------------------------
// 1. SMART DATES: Calcolo Scadenze Default
// -----------------------------------------
DateTime? defaultDate; DateTime? defaultDate;
final now = DateTime.now(); final now = DateTime.now();
if (type == 'Energy') { if (newType == 'Energy') {
defaultDate = DateTime(now.year, now.month + 24, now.day); defaultDate = DateTime(now.year, now.month + 24, now.day);
} }
if (type == 'Fin') { if (newType == 'Fin') {
defaultDate = DateTime(now.year, now.month + 30, now.day); defaultDate = DateTime(now.year, now.month + 30, now.day);
} }
if (type == 'Entertainment') { if (newType == 'Entertainment') {
defaultDate = DateTime(now.year, now.month + 12, now.day); defaultDate = DateTime(now.year, now.month + 12, now.day);
} }
updateFields( // -----------------------------------------
type: type, // 2. SMART PROVIDER: Filtro e Auto-Selezione
expirationDate: defaultDate, // -----------------------------------------
clearProvider: true, String? newProviderId = currentOp.providerId;
clearSubtype: true, String? newProviderName = currentOp.providerDisplayName;
clearModel: true,
clearQuantity: true, // A) Il provider attuale è ancora compatibile col nuovo tipo scelto?
if (newProviderId != null && newProviderId.isNotEmpty) {
final currentProvider = allProviders
.where((p) => p.id == newProviderId)
.firstOrNull;
if (currentProvider == null ||
!currentProvider.supportsOperation(newType)) {
// Non è più compatibile (es. da TIM fisso passo a Energy). Lo sbianchiamo!
newProviderId = null;
newProviderName = null;
}
}
// B) Se non c'è un provider selezionato, proviamo ad auto-inserire quello di default del negozio
if ((newProviderId == null || newProviderId.isEmpty) &&
defaultProviderId != null) {
final defaultProvider = allProviders
.where((p) => p.id == defaultProviderId)
.firstOrNull;
// Controlliamo che il default del negozio supporti questa specifica operazione
if (defaultProvider != null &&
defaultProvider.supportsOperation(newType)) {
newProviderId = defaultProvider.id;
newProviderName = defaultProvider.name;
}
}
// -----------------------------------------
// 3. EMISSIONE DELLO STATO PULITO
// -----------------------------------------
emit(
state.copyWith(
operation: currentOp.copyWith(
type: newType,
subtype:
'', // Resettiamo il sottotipo per evitare incongruenze (es. passo da Luce a DAZN)
expirationDate:
defaultDate, // Impostiamo la scadenza di default se calcolata
providerId: newProviderId,
providerDisplayName: newProviderName,
modelId: null,
modelDisplayName: null,
),
),
); );
} }
} }

View File

@@ -1,7 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/blocs/session/session_cubit.dart';
import 'package:flux/core/widgets/shared_forms/attachments_section.dart'; import 'package:flux/core/widgets/shared_forms/attachments_section.dart';
import 'package:flux/features/attachments/blocs/attachments_bloc.dart'; import 'package:flux/features/attachments/blocs/attachments_bloc.dart';
import 'package:flux/features/master_data/providers/blocs/provider_list_cubit.dart';
import 'package:flux/features/operations/blocs/operation_form_cubit.dart'; import 'package:flux/features/operations/blocs/operation_form_cubit.dart';
import 'package:flux/features/operations/models/operation_model.dart'; import 'package:flux/features/operations/models/operation_model.dart';
import 'package:flux/core/widgets/shared_forms/customer_section.dart'; import 'package:flux/core/widgets/shared_forms/customer_section.dart';
@@ -527,8 +529,24 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
selected: state.operation.type == type, selected: state.operation.type == type,
onSelected: (selected) { onSelected: (selected) {
if (selected) { if (selected) {
context.read<OperationFormCubit>().setTypeWithSmartDefault( // 1. Recuperiamo i provider caricati in memoria
type, final allProviders = context
.read<ProviderListCubit>()
.state
.providers;
// 2. Recuperiamo il provider di default del negozio dalla sessione
final defaultProviderId = context
.read<SessionCubit>()
.state
.currentStore
?.defaultProviderId;
// 3. Spariamo tutto nel metodo "tuttofare"
context.read<OperationFormCubit>().setTypeWithSmartDefaults(
newType: type,
allProviders: allProviders,
defaultProviderId: defaultProviderId,
); );
} }
}, },

View File

@@ -2,8 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/widgets/shared_forms/model_section.dart'; import 'package:flux/core/widgets/shared_forms/model_section.dart';
import 'package:flux/features/master_data/providers/blocs/provider_list_cubit.dart'; import 'package:flux/features/master_data/providers/blocs/provider_list_cubit.dart';
import 'package:flux/features/master_data/providers/models/provider_model.dart'; import 'package:flux/features/master_data/providers/models/provider_model_extensions.dart';
import 'package:flux/features/master_data/providers/models/provider_role.dart';
import 'package:flux/features/operations/blocs/operation_form_cubit.dart'; import 'package:flux/features/operations/blocs/operation_form_cubit.dart';
import 'package:flux/features/operations/models/operation_model.dart'; import 'package:flux/features/operations/models/operation_model.dart';
@@ -23,34 +22,6 @@ class OperationDetailsSection extends StatelessWidget {
required this.durationQuickPicks, required this.durationQuickPicks,
}); });
bool _doesProviderMatchOperationType(
ProviderModel provider,
String operationType,
) {
if (operationType == 'Altro') return true;
// Controlliamo che il fornitore abbia il ruolo specifico nel suo array
switch (operationType) {
case 'AL' || 'MNP':
return provider.roles.contains(ProviderRole.mobile);
case 'NIP' || 'FWA':
return provider.roles.contains(ProviderRole.landline);
case 'UNICA':
return provider.roles.contains(ProviderRole.landline) ||
provider.roles.contains(ProviderRole.mobile);
case 'Energy':
return provider.roles.contains(ProviderRole.energy);
case 'Fin':
return provider.roles.contains(ProviderRole.financing);
case 'Entertainment':
return provider.roles.contains(ProviderRole.entertainment);
case 'TELEPASS':
return provider.roles.contains(ProviderRole.telepass);
default:
return true;
}
}
void _showProviderModal(BuildContext context, String operationType) { void _showProviderModal(BuildContext context, String operationType) {
final OperationFormCubit cubit = context.read<OperationFormCubit>(); final OperationFormCubit cubit = context.read<OperationFormCubit>();
showModalBottomSheet( showModalBottomSheet(
@@ -92,14 +63,9 @@ class OperationDetailsSection extends StatelessWidget {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
// Prendiamo i provider e li filtriamo per ruolo e per stato attivo // 🥷 IL TOCCO DEL NINJA: Filtriamo usando direttamente l'Extension sul Modello!
final filteredProviders = state.providers.where((p) { final filteredProviders = state.providers.where((p) {
final isMatch = _doesProviderMatchOperationType( return p.supportsOperation(operationType) && p.isActive;
p,
operationType,
);
return isMatch &&
p.isActive; // Mostriamo solo quelli attivi!
}).toList(); }).toList();
if (filteredProviders.isEmpty) { if (filteredProviders.isEmpty) {

View File

@@ -1,7 +1,7 @@
name: flux name: flux
description: "Gestione attività negozio di telefonia" description: "Gestione attività negozio di telefonia"
publish_to: 'none' publish_to: 'none'
version: 1.1.12+30 version: 1.1.13+31
environment: environment:
sdk: ^3.11.3 sdk: ^3.11.3