default provider
This commit is contained in:
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user