refactor providers e basi per spedizioni

This commit is contained in:
2026-05-15 10:12:05 +02:00
parent ad35f641b3
commit f19f19a279
21 changed files with 1542 additions and 830 deletions

View File

@@ -20,8 +20,11 @@ import 'package:flux/features/home/ui/home_screen.dart';
import 'package:flux/features/master_data/master_data_hub_content.dart'; import 'package:flux/features/master_data/master_data_hub_content.dart';
import 'package:flux/features/master_data/products/blocs/product_cubit.dart'; import 'package:flux/features/master_data/products/blocs/product_cubit.dart';
import 'package:flux/features/master_data/products/ui/products_screen.dart'; import 'package:flux/features/master_data/products/ui/products_screen.dart';
import 'package:flux/features/master_data/providers/blocs/provider_cubit.dart'; import 'package:flux/features/master_data/providers/blocs/provider_form_cubit.dart';
import 'package:flux/features/master_data/providers/ui/providers_master_data_screen.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/ui/provider_form_screen.dart';
import 'package:flux/features/master_data/providers/ui/provider_list_screen.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/master_data/staff/ui/staff_screen.dart'; import 'package:flux/features/master_data/staff/ui/staff_screen.dart';
import 'package:flux/features/master_data/store/ui/stores_screen.dart'; import 'package:flux/features/master_data/store/ui/stores_screen.dart';
@@ -159,10 +162,9 @@ class AppRouter {
builder: (context, state) => const StoresScreen(), builder: (context, state) => const StoresScreen(),
), ),
GoRoute( GoRoute(
path: 'providers', path: '/providers',
name: Routes.providers, // Diventa /master-data/providers name: Routes.providers,
builder: (context, state) => builder: (context, state) => const ProviderListScreen(),
const ProvidersMasterDataScreen(),
), ),
], ],
), ),
@@ -200,6 +202,20 @@ class AppRouter {
), ),
// --- DETTAGLI E OPERATIVITÀ (FUORI DALLA SHELL - TUTTO SCHERMO) --- // --- DETTAGLI E OPERATIVITÀ (FUORI DALLA SHELL - TUTTO SCHERMO) ---
GoRoute(
path: '/providers/form',
name: Routes.providerForm,
builder: (context, state) {
// Estraiamo il fornitore (se stiamo modificando e non creando)
final existingProvider = state.extra as ProviderModel?;
return BlocProvider<ProviderFormCubit>(
// Inizializziamo un Cubit NUOVO ogni volta che apriamo il form
create: (context) => ProviderFormCubit(),
child: ProviderFormScreen(existingProvider: existingProvider),
);
},
),
GoRoute( GoRoute(
// Il path sarà es. /tickets/form/123 oppure /tickets/form/new // Il path sarà es. /tickets/form/123 oppure /tickets/form/new
path: '/tickets/form/:id', path: '/tickets/form/:id',
@@ -335,9 +351,7 @@ class AppRouter {
.currentStore! .currentStore!
.id!; .id!;
context.read<CustomersCubit>().loadCustomers(); context.read<CustomersCubit>().loadCustomers();
context.read<ProvidersCubit>().loadActiveProvidersForStore( context.read<ProviderListCubit>().loadProviders(currentStoreId);
currentStoreId,
);
context.read<ProductsCubit>().loadModels(); context.read<ProductsCubit>().loadModels();
context.read<ProductsCubit>().loadBrands(); context.read<ProductsCubit>().loadBrands();
return MultiBlocProvider( return MultiBlocProvider(

View File

@@ -9,6 +9,7 @@ class Routes {
static const String staff = 'staff'; static const String staff = 'staff';
static const String stores = 'stores'; static const String stores = 'stores';
static const String providers = 'providers'; static const String providers = 'providers';
static const String providerForm = 'provider-form';
static const String settings = 'settings'; static const String settings = 'settings';
static const String themeSettings = 'themeSettings'; static const String themeSettings = 'themeSettings';
static const String operations = 'operations'; static const String operations = 'operations';

View File

@@ -0,0 +1,64 @@
import 'package:equatable/equatable.dart';
class ShipmentDocumentModel extends Equatable {
final String? id;
final String companyId;
final String ticketId;
final String providerId;
final String destinationLocationId;
final String docNumber;
final DateTime docDate;
final int packageCount;
final double? weight;
final String shippingReason;
final String? notes;
const ShipmentDocumentModel({
this.id,
required this.companyId,
required this.ticketId,
required this.providerId,
required this.destinationLocationId,
required this.docNumber,
required this.docDate,
this.packageCount = 1,
this.weight,
this.shippingReason = 'Riparazione esterna',
this.notes,
});
factory ShipmentDocumentModel.fromMap(Map<String, dynamic> map) {
return ShipmentDocumentModel(
id: map['id'],
companyId: map['company_id'],
ticketId: map['ticket_id'],
providerId: map['provider_id'],
destinationLocationId: map['destination_location_id'],
docNumber: map['doc_number'],
docDate: DateTime.parse(map['doc_date']),
packageCount: map['package_count'] ?? 1,
weight: map['weight'] != null ? (map['weight'] as num).toDouble() : null,
shippingReason: map['shipping_reason'] ?? 'Riparazione esterna',
notes: map['notes'],
);
}
Map<String, dynamic> toMap() {
return {
if (id != null) 'id': id,
'company_id': companyId,
'ticket_id': ticketId,
'provider_id': providerId,
'destination_location_id': destinationLocationId,
'doc_number': docNumber,
'doc_date': docDate.toIso8601String(),
'package_count': packageCount,
'weight': weight,
'shipping_reason': shippingReason,
'notes': notes,
};
}
@override
List<Object?> get props => [id, docNumber, ticketId];
}

View File

@@ -1,176 +0,0 @@
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/blocs/session/session_cubit.dart';
import 'package:flux/features/master_data/providers/data/provider_repository.dart';
import 'package:flux/features/master_data/store/models/store_model.dart';
import 'package:get_it/get_it.dart';
import '../models/provider_model.dart';
class ProvidersState extends Equatable {
final List<ProviderModel> allProviders;
final List<String> associatedIds;
// NUOVO CAMPO: Lista dei provider pronti per essere usati nel form pratiche
final List<ProviderModel> activeProviders;
final bool isLoading;
final String? errorMessage;
const ProvidersState({
this.allProviders = const [],
this.associatedIds = const [],
this.activeProviders = const [], // Inizializza
this.isLoading = false,
this.errorMessage,
});
ProvidersState copyWith({
List<ProviderModel>? allProviders,
List<String>? associatedIds,
List<ProviderModel>? activeProviders, // Aggiungi qui
bool? isLoading,
String? errorMessage,
}) {
return ProvidersState(
allProviders: allProviders ?? this.allProviders,
associatedIds: associatedIds ?? this.associatedIds,
activeProviders: activeProviders ?? this.activeProviders, // Aggiungi qui
isLoading: isLoading ?? this.isLoading,
errorMessage:
errorMessage ??
this.errorMessage, // Correzione bug: mancava "?? this.errorMessage" nel tuo originale
);
}
@override
List<Object?> get props => [
allProviders,
associatedIds,
activeProviders, // Aggiungi qui
isLoading,
errorMessage,
];
}
class ProvidersCubit extends Cubit<ProvidersState> {
final ProviderRepository _repository = GetIt.I<ProviderRepository>();
final SessionCubit _sessionCubit = GetIt.I<SessionCubit>();
ProvidersCubit() : super(const ProvidersState());
// Carica i provider della company e quelli associati a uno store specifico
Future<void> loadProviders({StoreModel? store}) async {
emit(state.copyWith(isLoading: true));
try {
final all = await _repository.fetchAllCompanyProviders(
_sessionCubit.state.company!.id!,
);
List<String> associated = [];
if (store != null) {
associated = await _repository.fetchAssociatedProviderIds(store.id!);
}
emit(
state.copyWith(
allProviders: all,
associatedIds: associated,
isLoading: false,
),
);
} catch (e) {
emit(state.copyWith(isLoading: false, errorMessage: e.toString()));
}
}
Future<void> loadActiveProvidersForStore(String storeId) async {
emit(state.copyWith(isLoading: true));
try {
final activeList = await _repository.fetchActiveProvidersForStore(
storeId,
);
emit(state.copyWith(activeProviders: activeList, isLoading: false));
} catch (e) {
emit(
state.copyWith(
isLoading: false,
errorMessage: "Errore caricamento gestori: $e",
),
);
}
}
// Aggiunge o rimuove l'associazione con lo store
Future<void> toggleProviderAssociation({
required String providerId,
required String storeId,
required bool isCurrentlyAssociated,
}) async {
try {
if (isCurrentlyAssociated) {
await _repository.disassociateProviderFromStore(
providerId: providerId,
storeId: storeId,
);
// Aggiorniamo lo stato locale rimuovendo l'ID
final newIds = List<String>.from(state.associatedIds)
..remove(providerId);
emit(state.copyWith(associatedIds: newIds));
} else {
await _repository.associateProviderToStore(
providerId: providerId,
storeId: storeId,
);
// Aggiorniamo lo stato locale aggiungendo l'ID
final newIds = List<String>.from(state.associatedIds)..add(providerId);
emit(state.copyWith(associatedIds: newIds));
}
} catch (e) {
emit(state.copyWith(errorMessage: "Errore durante l'aggiornamento: $e"));
}
}
// Salvataggio/Update anagrafica (nuovo o modifica)
Future<void> saveProvider(
ProviderModel provider,
List<String> selectedStoreIds,
) async {
emit(state.copyWith(isLoading: true));
// Assicuriamoci di settare la companyId prima di salvare
provider = provider.copyWith(companyId: _sessionCubit.state.company!.id);
try {
// 1. Salviamo l'anagrafica (upsert)
// Se è un nuovo provider, l'ID potrebbe essere generato qui dal DB
// Quindi carichiamo il risultato del salvataggio per avere l'ID
final response = await _repository.saveProvider(provider);
// Assumiamo che il saveProvider restituisca l'oggetto salvato con l'ID
final pId = provider.id ?? response.id;
// 2. Sincronizziamo i negozi
await _repository.syncProviderStores(pId!, selectedStoreIds);
// 3. Ricarichiamo tutto
await loadProviders();
} catch (e) {
emit(state.copyWith(isLoading: false, errorMessage: e.toString()));
}
}
Future<void> saveProviderWithStores(
ProviderModel provider,
List<String> storeIds,
) async {
emit(state.copyWith(isLoading: true));
try {
// 1. Salva l'anagrafica provider
await _repository.saveProvider(provider);
// 2. Sincronizza i negozi (la via più semplice è cancellare e reinserire
// o fare un confronto tra i presenti e i nuovi)
await _repository.syncProviderStores(provider.id!, storeIds);
await loadProviders();
} catch (e) {
emit(state.copyWith(isLoading: false, errorMessage: e.toString()));
}
}
}

View File

@@ -0,0 +1,182 @@
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart';
import 'package:supabase_flutter/supabase_flutter.dart'; // Per estrarre gli store
import '../models/provider_model.dart';
import '../models/provider_role.dart';
import '../models/provider_location_model.dart';
import '../data/provider_repository.dart';
part 'provider_form_state.dart';
class ProviderFormCubit extends Cubit<ProviderFormState> {
final ProviderRepository _repository = GetIt.I.get<ProviderRepository>();
final _client = Supabase.instance.client; // Lo usiamo al volo per gli store
ProviderFormCubit()
: super(ProviderFormState(provider: ProviderModel.empty(companyId: '')));
// --- INIZIALIZZAZIONE ---
Future<void> initForm({
required String companyId,
ProviderModel? existingProvider,
}) async {
emit(state.copyWith(status: ProviderFormStatus.loading));
try {
// 1. Scarichiamo tutti i negozi dell'azienda
final storesResponse = await _client
.from('store')
.select('id, name')
.eq('company_id', companyId);
// 2. Se stiamo modificando, carichiamo gli store collegati
List<String> linkedStoreIds = [];
if (existingProvider != null && existingProvider.id != null) {
// ... (Vecchio codice di recupero)
final links = await _client
.from('providers_in_stores')
.select('store_id')
.eq('provider_id', existingProvider.id!);
linkedStoreIds = (links as List)
.map((l) => l['store_id'] as String)
.toList();
} else {
// --- IL TOCCO NINJA: AUTO-SELEZIONE ---
// Se stiamo creando un nuovo fornitore e c'è 1 solo negozio in tutto il DB, accendilo!
if ((storesResponse as List).length == 1) {
linkedStoreIds.add(storesResponse.first['id'] as String);
}
}
emit(
state.copyWith(
status: ProviderFormStatus.initial,
provider: existingProvider ?? ProviderModel.empty(companyId: ''),
availableStores: storesResponse as List<dynamic>,
selectedStoreIds: linkedStoreIds,
localLocations: existingProvider?.locations ?? [],
),
);
} catch (e) {
emit(
state.copyWith(
status: ProviderFormStatus.failure,
errorMessage: 'Errore durante l\'inizializzazione: $e',
),
);
}
}
// --- AGGIORNAMENTO CAMPI ---
void updateFields({
String? name,
String? businessName,
String? vatNumber,
String? fiscalCode,
String? sdiCode,
String? emailPec,
}) {
emit(
state.copyWith(
provider: state.provider.copyWith(
name: name,
businessName: businessName,
vatNumber: vatNumber,
fiscalCode: fiscalCode,
sdiCode: sdiCode,
emailPec: emailPec,
),
),
);
}
// --- GESTIONE RUOLI (CHIPS) ---
void toggleRole(ProviderRole role) {
final currentRoles = List<ProviderRole>.from(state.provider.roles);
if (currentRoles.contains(role)) {
currentRoles.remove(role);
} else {
currentRoles.add(role);
}
emit(
state.copyWith(provider: state.provider.copyWith(roles: currentRoles)),
);
}
// --- GESTIONE NEGOZI ABILITATI (CHECKBOX) ---
void toggleStore(String storeId) {
final currentStoreIds = List<String>.from(state.selectedStoreIds);
if (currentStoreIds.contains(storeId)) {
currentStoreIds.remove(storeId);
} else {
currentStoreIds.add(storeId);
}
emit(state.copyWith(selectedStoreIds: currentStoreIds));
}
Future<void> addLocationLocal(ProviderLocationModel location) async {
final currentLocations = List<ProviderLocationModel>.from(
state.localLocations,
);
currentLocations.add(location);
emit(state.copyWith(localLocations: currentLocations));
}
void removeLocationLocal(int index) {
final currentLocations = List<ProviderLocationModel>.from(
state.localLocations,
);
if (index >= 0 && index < currentLocations.length) {
currentLocations.removeAt(index);
emit(state.copyWith(localLocations: currentLocations));
}
}
// --- SALVATAGGIO FINALE ---
Future<void> save() async {
// Sicurezza di base
if (state.provider.name.trim().isEmpty) {
emit(
state.copyWith(
status: ProviderFormStatus.failure,
errorMessage: 'Il nome è obbligatorio',
),
);
return;
}
emit(state.copyWith(status: ProviderFormStatus.loading));
try {
// Passiamo provider e storeId al repository che farà la magia
final savedProvider = await _repository.saveProvider(
state.provider,
state.selectedStoreIds,
);
if (state.localLocations.isNotEmpty) {
for (var loc in state.localLocations) {
final locToSave = loc.copyWith(
providerId: savedProvider.id!,
companyId: savedProvider.companyId,
);
await _repository.saveLocation(locToSave);
}
}
emit(
state.copyWith(
status: ProviderFormStatus.success,
provider: savedProvider,
),
);
} catch (e) {
emit(
state.copyWith(
status: ProviderFormStatus.failure,
errorMessage: 'Errore di salvataggio: $e',
),
);
}
}
}

View File

@@ -0,0 +1,55 @@
part of 'provider_form_cubit.dart';
// Importa il tuo StoreModel se lo hai
enum ProviderFormStatus { initial, loading, success, failure }
class ProviderFormState extends Equatable {
final ProviderFormStatus status;
final ProviderModel provider;
// Dati di supporto per l'interfaccia
final List<dynamic>
availableStores; // Metti List<StoreModel> se hai il modello
final List<String> selectedStoreIds; // IDs dei negozi in cui è attivo
final List<ProviderLocationModel>
localLocations; // Sedi aggiunte prima del salvataggio
final String? errorMessage;
const ProviderFormState({
this.status = ProviderFormStatus.initial,
required this.provider,
this.availableStores = const [],
this.selectedStoreIds = const [],
this.localLocations = const [],
this.errorMessage,
});
ProviderFormState copyWith({
ProviderFormStatus? status,
ProviderModel? provider,
List<dynamic>? availableStores,
List<String>? selectedStoreIds,
List<ProviderLocationModel>? localLocations,
String? errorMessage,
}) {
return ProviderFormState(
status: status ?? this.status,
provider: provider ?? this.provider,
availableStores: availableStores ?? this.availableStores,
selectedStoreIds: selectedStoreIds ?? this.selectedStoreIds,
localLocations: localLocations ?? this.localLocations,
errorMessage: errorMessage ?? this.errorMessage,
);
}
@override
List<Object?> get props => [
status,
provider,
availableStores,
selectedStoreIds,
localLocations,
errorMessage,
];
}

View File

@@ -0,0 +1,53 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:get_it/get_it.dart';
import '../models/provider_model.dart';
import '../data/provider_repository.dart';
part 'provider_list_state.dart';
class ProviderListCubit extends Cubit<ProviderListState> {
final ProviderRepository _repository = GetIt.I.get<ProviderRepository>();
ProviderListCubit() : super(const ProviderListState());
Future<void> loadProviders(String storeId) async {
emit(state.copyWith(status: ProviderListStatus.loading));
try {
final providers = await _repository.getProvidersByStore(storeId);
emit(
state.copyWith(
status: ProviderListStatus.success,
providers: providers,
),
);
} catch (e) {
emit(
state.copyWith(
status: ProviderListStatus.failure,
errorMessage: e.toString(),
),
);
}
}
Future<void> loadAllProviders() async {
emit(state.copyWith(status: ProviderListStatus.loading));
try {
final allProviders = await _repository.getAllCompanyProviders();
emit(
state.copyWith(
status: ProviderListStatus.success,
allProviders: allProviders,
),
);
} catch (e) {
emit(
state.copyWith(
status: ProviderListStatus.failure,
errorMessage: e.toString(),
),
);
}
}
}

View File

@@ -0,0 +1,34 @@
part of 'provider_list_cubit.dart';
enum ProviderListStatus { initial, loading, success, failure }
class ProviderListState extends Equatable {
final ProviderListStatus status;
final List<ProviderModel> providers;
final List<ProviderModel> allProviders;
final String? errorMessage;
const ProviderListState({
this.status = ProviderListStatus.initial,
this.providers = const [],
this.allProviders = const [],
this.errorMessage,
});
ProviderListState copyWith({
ProviderListStatus? status,
List<ProviderModel>? providers,
List<ProviderModel>? allProviders,
String? errorMessage,
}) {
return ProviderListState(
status: status ?? this.status,
providers: providers ?? this.providers,
allProviders: allProviders ?? this.allProviders,
errorMessage: errorMessage ?? this.errorMessage,
);
}
@override
List<Object?> get props => [status, providers, allProviders, errorMessage];
}

View File

@@ -1,137 +1,81 @@
import 'package:get_it/get_it.dart';
import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:supabase_flutter/supabase_flutter.dart';
import '../models/provider_model.dart'; import '../models/provider_model.dart';
import '../models/provider_location_model.dart';
class ProviderRepository { class ProviderRepository {
final _supabase = Supabase.instance.client; final _supabase = GetIt.I.get<SupabaseClient>();
// --- ASSOCIAZIONE PROVIDER <-> STORE --- // 1. Carica i provider abilitati per uno specifico Store
Future<List<ProviderModel>> getProvidersByStore(String storeId) async {
// Aggiunge un provider a un negozio (Attiva mandato) final response = await _supabase
Future<void> associateProviderToStore({ .from('providers_in_stores')
required String providerId, .select('''
required String storeId, provider_id,
}) async { provider:provider (
try { *,
await _supabase.from('providers_in_stores').insert({ provider_locations (*)
'provider_id': providerId,
'store_id': storeId,
});
} catch (e) {
throw Exception('Errore durante l\'associazione provider: $e');
}
}
// Rimuove un provider da un negozio (Disattiva mandato)
Future<void> disassociateProviderFromStore({
required String providerId,
required String storeId,
}) async {
try {
await _supabase
.from('providers_in_stores')
.delete()
.eq('provider_id', providerId)
.eq('store_id', storeId);
} catch (e) {
throw Exception('Errore durante la disassociazione provider: $e');
}
}
// Recupera tutti i provider di una company (per la lista generale)
Future<List<ProviderModel>> fetchAllCompanyProviders(String companyId) async {
try {
final response = await _supabase
.from('provider')
.select('''
*,
associated_stores:providers_in_stores (
store (
*
)
) )
''') ''')
.eq('company_id', companyId) .eq('store_id', storeId)
.order('name'); .order('name', referencedTable: 'provider');
return (response as List).map((m) => ProviderModel.fromMap(m)).toList(); // Mappiamo i risultati estraendo l'oggetto 'provider' annidato
} catch (e) { return (response as List).map((row) {
throw 'Errore fetch providers: $e'; return ProviderModel.fromMap(row['provider'] as Map<String, dynamic>);
} }).toList();
} }
// Recupera gli ID dei provider associati a uno store (utile per le checkbox) // 2. Carica TUTTI i provider della Company (per la gestione anagrafica)
Future<List<String>> fetchAssociatedProviderIds(String storeId) async { Future<List<ProviderModel>> getAllCompanyProviders() async {
try { final response = await _supabase
final response = await _supabase .from('provider')
.from('providers_in_stores') .select('*, provider_locations (*)')
.select('provider_id') .order('name');
.eq('store_id', storeId);
return (response as List) return (response as List)
.map((item) => item['provider_id'].toString()) .map((row) => ProviderModel.fromMap(row as Map<String, dynamic>))
.toList();
}
// 3. Salvataggio atomico (Upsert) del Provider
Future<ProviderModel> saveProvider(
ProviderModel provider,
List<String> enabledStoreIds,
) async {
// A. Salva/Aggiorna il Provider principale
final savedRow = await _supabase
.from('provider')
.upsert(provider.toMap())
.select()
.single();
final savedProvider = ProviderModel.fromMap(savedRow);
// B. Sincronizza gli Store (Cancelliamo i vecchi e mettiamo i nuovi per semplicità)
// In un'app ad alto traffico faremmo un confronto, qui l'upsert totale è più veloce da scrivere.
await _supabase
.from('providers_in_stores')
.delete()
.eq('provider_id', savedProvider.id!);
if (enabledStoreIds.isNotEmpty) {
final storeLinks = enabledStoreIds
.map((sId) => {'provider_id': savedProvider.id, 'store_id': sId})
.toList(); .toList();
} catch (e) {
throw Exception('Errore recupero ID associati: $e'); await _supabase.from('providers_in_stores').insert(storeLinks);
} }
return savedProvider;
} }
// --- FUNZIONI STANDARD --- // 4. Gestione Sedi (Locations)
Future<void> saveLocation(ProviderLocationModel location) async {
// Questa la userai nel Form Servizi: carica solo i provider abilitati per lo store await _supabase.from('provider_locations').upsert(location.toMap());
Future<List<ProviderModel>> fetchActiveProvidersForStore(
String storeId,
) async {
try {
final response = await _supabase
.from('provider')
.select('*, providers_in_stores!inner(store_id)')
.eq('providers_in_stores.store_id', storeId)
.eq('is_active', true);
return (response as List).map((m) => ProviderModel.fromMap(m)).toList();
} catch (e) {
throw Exception('Errore fetch provider attivi: $e');
}
} }
// Salva o aggiorna l'anagrafica del Provider Future<void> deleteLocation(String locationId) async {
Future<ProviderModel> saveProvider(ProviderModel provider) async { await _supabase.from('provider_locations').delete().eq('id', locationId);
try {
// .select().single() è fondamentale per farsi restituire
// l'oggetto appena creato/aggiornato con l'ID
final response = await _supabase
.from('provider')
.upsert(provider.toMap())
.select()
.single();
return ProviderModel.fromMap(response); // <--- DEVE ESSERCI IL RETURN
} catch (e) {
rethrow; // <--- Rilancia l'errore al Cubit, non ritornare null!
}
}
Future<void> syncProviderStores(
String providerId,
List<String> storeIds,
) async {
try {
// 1. Eliminiamo tutte le associazioni correnti per questo provider
await _supabase
.from('providers_in_stores')
.delete()
.eq('provider_id', providerId);
// 2. Se ci sono nuovi store da associare, li inseriamo
if (storeIds.isNotEmpty) {
final inserts = storeIds
.map((sId) => {'provider_id': providerId, 'store_id': sId})
.toList();
await _supabase.from('providers_in_stores').insert(inserts);
}
} catch (e) {
throw Exception('Errore durante la sincronizzazione store: $e');
}
} }
} }

View File

@@ -0,0 +1,109 @@
import 'package:equatable/equatable.dart';
class ProviderLocationModel extends Equatable {
final String? id;
final String providerId;
final String companyId;
final String name; // Es: "Laboratorio Centrale"
final String address;
final String city;
final String zipCode;
final String province;
final String? contactPerson;
final bool isMain;
const ProviderLocationModel({
this.id,
required this.providerId,
required this.companyId,
required this.name,
required this.address,
required this.city,
required this.zipCode,
required this.province,
this.contactPerson,
this.isMain = false,
});
factory ProviderLocationModel.empty() {
return const ProviderLocationModel(
providerId: '',
companyId: '',
name: '',
address: '',
city: '',
zipCode: '',
province: '',
);
}
ProviderLocationModel copyWith({
String? id,
String? providerId,
String? companyId,
String? name,
String? address,
String? city,
String? zipCode,
String? province,
String? contactPerson,
bool? isMain,
}) {
return ProviderLocationModel(
id: id ?? this.id,
providerId: providerId ?? this.providerId,
companyId: companyId ?? this.companyId,
name: name ?? this.name,
address: address ?? this.address,
city: city ?? this.city,
zipCode: zipCode ?? this.zipCode,
province: province ?? this.province,
contactPerson: contactPerson ?? this.contactPerson,
isMain: isMain ?? this.isMain,
);
}
factory ProviderLocationModel.fromMap(Map<String, dynamic> map) {
return ProviderLocationModel(
id: map['id'] as String,
providerId: map['provider_id'] as String,
companyId: map['company_id'] as String,
name: map['name'] as String,
address: map['address'] as String,
city: map['city'] as String,
zipCode: map['zip_code'] as String,
province: map['province'] as String,
contactPerson: map['contact_person'] as String?,
isMain: map['is_main'] as bool? ?? false,
);
}
Map<String, dynamic> toMap() {
return {
if (id != null) 'id': id,
'provider_id': providerId,
'company_id': companyId,
'name': name,
'address': address,
'city': city,
'zip_code': zipCode,
'province': province,
'contact_person': contactPerson,
'is_main': isMain,
};
}
@override
List<Object?> get props => [
id,
providerId,
companyId,
name,
address,
city,
zipCode,
province,
contactPerson,
isMain,
];
}

View File

@@ -1,134 +1,169 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flux/features/master_data/store/models/store_model.dart';
import 'provider_location_model.dart';
import 'provider_role.dart';
class ProviderModel extends Equatable { class ProviderModel extends Equatable {
final String? id; final String? id;
final String name;
final bool landline;
final bool mobile;
final bool energy;
final bool insurance;
final bool entertainment;
final bool financing;
final bool telepass;
final bool other;
final bool isActive;
final String companyId; final String companyId;
final List<StoreModel> associatedStores; final String name; // Nome "commerciale" per riconoscerlo velocemente
final bool isActive;
// Dati fiscali e legali
final String? businessName; // Ragione Sociale
final String? vatNumber; // P.IVA
final String? fiscalCode; // C.F.
final String? sdiCode; // Codice Univoco (SDI)
final String? emailPec;
final String? legalAddress;
final String? legalCity;
final String? legalZip;
final String? legalProvince;
// Ruoli e Sedi (Relazioni)
final List<ProviderRole> roles;
final List<ProviderLocationModel>? locations;
const ProviderModel({ const ProviderModel({
this.id, this.id,
required this.name,
required this.landline,
required this.mobile,
required this.energy,
required this.insurance,
required this.entertainment,
required this.financing,
required this.telepass,
required this.other,
required this.isActive,
required this.companyId, required this.companyId,
this.associatedStores = const [], required this.name,
this.isActive = true,
this.businessName,
this.vatNumber,
this.fiscalCode,
this.sdiCode,
this.emailPec,
this.legalAddress,
this.legalCity,
this.legalZip,
this.legalProvince,
this.roles = const [],
this.locations,
}); });
factory ProviderModel.empty({required String companyId}) {
return ProviderModel(
companyId: companyId,
name: '',
isActive: true,
roles: const [],
);
}
ProviderModel copyWith({
String? id,
String? companyId,
String? name,
bool? isActive,
String? businessName,
String? vatNumber,
String? fiscalCode,
String? sdiCode,
String? emailPec,
String? legalAddress,
String? legalCity,
String? legalZip,
String? legalProvince,
List<ProviderRole>? roles,
List<ProviderLocationModel>? locations,
}) {
return ProviderModel(
id: id ?? this.id,
companyId: companyId ?? this.companyId,
name: name ?? this.name,
isActive: isActive ?? this.isActive,
businessName: businessName ?? this.businessName,
vatNumber: vatNumber ?? this.vatNumber,
fiscalCode: fiscalCode ?? this.fiscalCode,
sdiCode: sdiCode ?? this.sdiCode,
emailPec: emailPec ?? this.emailPec,
legalAddress: legalAddress ?? this.legalAddress,
legalCity: legalCity ?? this.legalCity,
legalZip: legalZip ?? this.legalZip,
legalProvince: legalProvince ?? this.legalProvince,
roles: roles ?? this.roles,
locations: locations ?? this.locations,
);
}
factory ProviderModel.fromMap(Map<String, dynamic> map) { factory ProviderModel.fromMap(Map<String, dynamic> map) {
// Estraiamo la lista dalla pivot e poi prendiamo l'oggetto 'store' annidato // Parsing sicuro dell'array testuale di Supabase per trasformarlo in Enum
final pivotList = map['associated_stores'] as List?; List<ProviderRole> parsedRoles = [];
List<StoreModel> stores = []; if (map['roles'] != null) {
if (pivotList != null) { final List<dynamic> rawRoles = map['roles'];
stores = pivotList for (var r in rawRoles) {
.where((item) => item['store'] != null) // Sicurezza final role = ProviderRole.fromString(r as String);
if (role != null) parsedRoles.add(role);
}
}
// Parsing della JOIN per le locations, se presenti nella query
List<ProviderLocationModel>? parsedLocations;
if (map['provider_locations'] != null) {
parsedLocations = (map['provider_locations'] as List<dynamic>)
.map( .map(
(item) => StoreModel.fromMap(item['store'] as Map<String, dynamic>), (item) =>
ProviderLocationModel.fromMap(item as Map<String, dynamic>),
) )
.toList(); .toList();
} }
return ProviderModel( return ProviderModel(
id: map['id'], id: map['id'] as String,
name: map['name'], companyId: map['company_id'] as String,
landline: map['landline'] ?? false, name: map['name'] as String,
mobile: map['mobile'] ?? false, isActive: map['is_active'] as bool? ?? true,
energy: map['energy'] ?? false, businessName: map['business_name'] as String?,
insurance: map['insurance'] ?? false, vatNumber: map['vat_number'] as String?,
entertainment: map['entertainment'] ?? false, fiscalCode: map['fiscal_code'] as String?,
financing: map['financing'] ?? false, sdiCode: map['sdi_code'] as String?,
telepass: map['telepass'] ?? false, emailPec: map['email_pec'] as String?,
other: map['other'] ?? false, legalAddress: map['legal_address'] as String?,
isActive: map['is_active'] ?? true, legalCity: map['legal_city'] as String?,
companyId: map['company_id'], legalZip: map['legal_zip'] as String?,
associatedStores: stores, legalProvince: map['legal_province'] as String?,
roles: parsedRoles,
locations: parsedLocations,
); );
} }
Map<String, dynamic> toMap() { Map<String, dynamic> toMap() {
final map = { return {
'name': name, if (id != null) 'id': id,
'landline': landline,
'mobile': mobile,
'energy': energy,
'insurance': insurance,
'entertainment': entertainment,
'financing': financing,
'telepass': telepass,
'other': other,
'is_active': isActive,
'company_id': companyId, 'company_id': companyId,
'name': name,
'is_active': isActive,
'business_name': businessName,
'vat_number': vatNumber,
'fiscal_code': fiscalCode,
'sdi_code': sdiCode,
'email_pec': emailPec,
'legal_address': legalAddress,
'legal_city': legalCity,
'legal_zip': legalZip,
'legal_province': legalProvince,
// Trasformiamo gli Enum di nuovo in stringhe per Supabase
'roles': roles.map((e) => e.name).toList(),
}; };
// AGGIUNGIAMO L'ID SOLO SE NON È NULLO
// Senza questo, l'upsert non sa dove andare a parare
if (id != null) {
map['id'] = id!;
}
return map;
} }
@override @override
List<Object?> get props => [ List<Object?> get props => [
id, id,
name,
landline,
mobile,
energy,
insurance,
entertainment,
financing,
telepass,
other,
isActive,
companyId, companyId,
associatedStores, name,
isActive,
businessName,
vatNumber,
fiscalCode,
sdiCode,
emailPec,
legalAddress,
legalCity,
legalZip,
legalProvince,
roles,
locations,
]; ];
ProviderModel copyWith({
String? id,
String? name,
bool? landline,
bool? mobile,
bool? energy,
bool? insurance,
bool? entertainment,
bool? financing,
bool? telepass,
bool? other,
bool? isActive,
String? companyId,
List<StoreModel>? associatedStores,
}) {
return ProviderModel(
id: id ?? this.id,
name: name ?? this.name,
landline: landline ?? this.landline,
mobile: mobile ?? this.mobile,
energy: energy ?? this.energy,
insurance: insurance ?? this.insurance,
entertainment: entertainment ?? this.entertainment,
financing: financing ?? this.financing,
telepass: telepass ?? this.telepass,
other: other ?? this.other,
isActive: isActive ?? this.isActive,
companyId: companyId ?? this.companyId,
associatedStores: associatedStores ?? this.associatedStores,
);
}
} }

View File

@@ -0,0 +1,28 @@
import 'package:flutter/material.dart';
enum ProviderRole {
landline('Fisso', Colors.blue),
mobile('Mobile', Colors.green),
energy('Energia', Colors.orange),
insurance('Assicurazioni', Colors.purple),
financing('Finanziamenti', Colors.teal),
entertainment('Intrattenimento', Colors.red),
telepass('Telepass', Colors.amber),
repairCenter('Centro Riparazioni', Colors.cyan),
partsSupplier('Fornitore Ricambi', Colors.indigo),
merchandiseSupplier('Fornitore Merce', Colors.brown);
final String displayValue;
final Color color; // <-- Il nostro tocco magico
const ProviderRole(this.displayValue, this.color);
static ProviderRole? fromString(String? value) {
if (value == null) return null;
try {
return ProviderRole.values.firstWhere((e) => e.name == value);
} catch (_) {
return null;
}
}
}

View File

@@ -0,0 +1,395 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/blocs/session/session_cubit.dart';
import 'package:flux/features/master_data/providers/blocs/provider_form_cubit.dart';
import 'package:flux/features/master_data/providers/models/provider_location_model.dart';
import 'package:flux/features/master_data/providers/models/provider_model.dart';
import 'package:flux/features/master_data/providers/models/provider_role.dart';
import 'package:flux/features/master_data/providers/ui/provider_location_dialog.dart';
class ProviderFormScreen extends StatefulWidget {
final ProviderModel? existingProvider;
const ProviderFormScreen({super.key, this.existingProvider});
@override
State<ProviderFormScreen> createState() => _ProviderFormScreenState();
}
class _ProviderFormScreenState extends State<ProviderFormScreen> {
final _formKey = GlobalKey<FormState>();
// Controllers per i campi di testo
late final TextEditingController _nameCtrl;
late final TextEditingController _businessNameCtrl;
late final TextEditingController _vatCtrl;
late final TextEditingController _cfCtrl;
late final TextEditingController _sdiCtrl;
late final TextEditingController _pecCtrl;
@override
void initState() {
super.initState();
final p = widget.existingProvider;
_nameCtrl = TextEditingController(text: p?.name);
_businessNameCtrl = TextEditingController(text: p?.businessName);
_vatCtrl = TextEditingController(text: p?.vatNumber);
_cfCtrl = TextEditingController(text: p?.fiscalCode);
_sdiCtrl = TextEditingController(text: p?.sdiCode);
_pecCtrl = TextEditingController(text: p?.emailPec);
// Inizializziamo il Cubit appena la schermata si apre
WidgetsBinding.instance.addPostFrameCallback((_) {
// Recupero il companyId dall'utente loggato (Vigile Urbano)
final companyId = context
.read<SessionCubit>()
.state
.currentStore!
.companyId;
context.read<ProviderFormCubit>().initForm(
companyId: companyId,
existingProvider: widget.existingProvider,
);
});
}
@override
void dispose() {
_nameCtrl.dispose();
_businessNameCtrl.dispose();
_vatCtrl.dispose();
_cfCtrl.dispose();
_sdiCtrl.dispose();
_pecCtrl.dispose();
super.dispose();
}
void _flushControllers() {
context.read<ProviderFormCubit>().updateFields(
name: _nameCtrl.text.trim(),
businessName: _businessNameCtrl.text.trim(),
vatNumber: _vatCtrl.text.trim(),
fiscalCode: _cfCtrl.text.trim(),
sdiCode: _sdiCtrl.text.trim(),
emailPec: _pecCtrl.text.trim(),
);
}
@override
Widget build(BuildContext context) {
final isEditing = widget.existingProvider != null;
return BlocConsumer<ProviderFormCubit, ProviderFormState>(
listenWhen: (previous, current) => previous.status != current.status,
listener: (context, state) {
if (state.status == ProviderFormStatus.success) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Fornitore salvato con successo!')),
);
Navigator.of(context).pop(); // Torna alla lista
} else if (state.status == ProviderFormStatus.failure) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.errorMessage ?? 'Errore di salvataggio'),
backgroundColor: Colors.red,
),
);
}
},
builder: (context, state) {
if (state.status == ProviderFormStatus.loading &&
state.provider.id == null) {
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
return Scaffold(
appBar: AppBar(
title: Text(isEditing ? 'Modifica Fornitore' : 'Nuovo Fornitore'),
actions: [
FilledButton.icon(
onPressed: () {
if (_formKey.currentState!.validate()) {
_flushControllers();
context.read<ProviderFormCubit>().save();
}
},
icon: const Icon(Icons.save),
label: const Text('Salva'),
),
const SizedBox(width: 16),
],
),
body: Form(
key: _formKey,
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildGeneralCard(context, state),
const SizedBox(height: 24),
_buildRolesCard(context, state),
const SizedBox(height: 24),
_buildFiscalCard(context),
const SizedBox(height: 24),
_buildStoresCard(context, state),
const SizedBox(height: 24),
_buildLocationsCard(context, state),
],
),
),
),
);
},
);
}
// --- CARD 1: DATI GENERALI ---
Widget _buildGeneralCard(BuildContext context, ProviderFormState state) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Dati Generali',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
TextFormField(
controller: _nameCtrl,
decoration: const InputDecoration(
labelText: 'Nome Fornitore (Display Name) *',
prefixIcon: Icon(Icons.storefront),
),
validator: (val) =>
val == null || val.isEmpty ? 'Campo obbligatorio' : null,
),
const SizedBox(height: 16),
// Volendo qui puoi aggiungere lo Switch per "Attivo/Inattivo"
],
),
),
);
}
// --- CARD 2: RUOLI (I CHIPS NINJA) ---
Widget _buildRolesCard(BuildContext context, ProviderFormState state) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Ruoli e Servizi',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const Text(
'Seleziona cosa fa questo fornitore (puoi sceglierne più di uno):',
style: TextStyle(fontSize: 12, color: Colors.grey),
),
const SizedBox(height: 16),
Wrap(
spacing: 8.0,
runSpacing: 8.0,
children: ProviderRole.values.map((role) {
final isSelected = state.provider.roles.contains(role);
return FilterChip(
label: Text(role.displayValue),
selected: isSelected,
selectedColor: role.color.withValues(alpha: 0.2),
checkmarkColor: role.color,
side: BorderSide(color: role.color.withValues(alpha: 0.3)),
onSelected: (bool selected) {
context.read<ProviderFormCubit>().toggleRole(role);
},
);
}).toList(),
),
],
),
),
);
}
// --- CARD 3: DATI FISCALI ---
Widget _buildFiscalCard(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Dati Fiscali (Per DDT e Fatturazione)',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
TextFormField(
controller: _businessNameCtrl,
decoration: const InputDecoration(
labelText: 'Ragione Sociale (es. Tech SpA)',
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: TextFormField(
controller: _vatCtrl,
decoration: const InputDecoration(labelText: 'Partita IVA'),
),
),
const SizedBox(width: 16),
Expanded(
child: TextFormField(
controller: _cfCtrl,
decoration: const InputDecoration(
labelText: 'Codice Fiscale',
),
),
),
],
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: TextFormField(
controller: _sdiCtrl,
decoration: const InputDecoration(labelText: 'Codice SDI'),
),
),
const SizedBox(width: 16),
Expanded(
child: TextFormField(
controller: _pecCtrl,
decoration: const InputDecoration(labelText: 'Email PEC'),
),
),
],
),
],
),
),
);
}
// --- CARD 4: NEGOZI ABILITATI ---
Widget _buildStoresCard(BuildContext context, ProviderFormState state) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Negozi Abilitati',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const Text(
'In quali punti vendita deve apparire questo fornitore?',
style: TextStyle(fontSize: 12, color: Colors.grey),
),
const SizedBox(height: 16),
if (state.availableStores.isEmpty)
const Center(
child: Text(
'Nessun negozio trovato.',
style: TextStyle(fontStyle: FontStyle.italic),
),
)
else
...state.availableStores.map((storeMap) {
final storeId = storeMap['id'] as String;
final storeName = storeMap['name'] as String;
final isEnabled = state.selectedStoreIds.contains(storeId);
return SwitchListTile(
title: Text(storeName),
value: isEnabled,
onChanged: (bool val) {
context.read<ProviderFormCubit>().toggleStore(storeId);
},
);
}),
],
),
),
);
}
Widget _buildLocationsCard(BuildContext context, ProviderFormState state) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Sedi e Laboratori',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
IconButton.filledTonal(
onPressed: () async {
ProviderFormCubit providerFormCubit = context
.read<ProviderFormCubit>();
final res = await showDialog<ProviderLocationModel?>(
context: context,
builder: (context) => const ProviderLocationDialog(),
);
if (res != null) {
// Chiama il cubit per aggiungere localmente
providerFormCubit.addLocationLocal(res);
}
},
icon: const Icon(Icons.add_location_alt),
),
],
),
const SizedBox(height: 16),
if (state.localLocations.isEmpty)
const Text(
'Nessun indirizzo di spedizione inserito.',
style: TextStyle(fontStyle: FontStyle.italic),
)
else
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: state.localLocations.length,
itemBuilder: (context, index) {
final loc = state.localLocations[index];
return ListTile(
leading: Icon(
loc.isMain ? Icons.star : Icons.location_on,
color: loc.isMain ? Colors.amber : null,
),
title: Text(loc.name),
subtitle: Text(
'${loc.address}, ${loc.city} (${loc.province})',
),
trailing: IconButton(
icon: const Icon(Icons.delete_outline, color: Colors.red),
onPressed: () => context
.read<ProviderFormCubit>()
.removeLocationLocal(index),
),
);
},
),
],
),
),
);
}
}

View File

@@ -1,214 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/features/master_data/providers/blocs/provider_cubit.dart';
import 'package:flux/features/master_data/providers/models/provider_model.dart';
import 'package:flux/features/master_data/store/bloc/store_cubit.dart';
class ProviderFormSheet extends StatefulWidget {
final ProviderModel? initialProvider;
const ProviderFormSheet({super.key, this.initialProvider});
@override
State<ProviderFormSheet> createState() => _ProviderFormSheetState();
}
class _ProviderFormSheetState extends State<ProviderFormSheet> {
late TextEditingController _nameController;
late bool _landline;
late bool _mobile;
late bool _energy;
late bool _insurance;
late bool _entertainment;
late bool _financing;
late bool _telepass;
late bool _other;
late bool _isActive;
final List<String> _tempSelectedStoreIds =
[]; // Per gestire la selezione temporanea dei negozi
@override
void initState() {
super.initState();
final p = widget.initialProvider;
for (final store in p?.associatedStores ?? []) {
_tempSelectedStoreIds.add(store.id!);
}
_nameController = TextEditingController(text: p?.name ?? '');
_landline = p?.landline ?? false;
_mobile = p?.mobile ?? false;
_energy = p?.energy ?? false;
_insurance = p?.insurance ?? false;
_entertainment = p?.entertainment ?? false;
_financing = p?.financing ?? false;
_telepass = p?.telepass ?? false;
_other = p?.other ?? false;
_isActive = p?.isActive ?? true;
}
@override
void dispose() {
_nameController.dispose();
super.dispose();
}
void _save() {
if (_nameController.text.trim().isEmpty) {
return;
}
final cubit = context.read<ProvidersCubit>();
final provider = ProviderModel(
id: widget.initialProvider?.id, // Se nullo, Supabase farà insert
name: _nameController.text.trim(),
landline: _landline,
mobile: _mobile,
energy: _energy,
insurance: _insurance,
entertainment: _entertainment,
financing: _financing,
telepass: _telepass,
other: _other,
isActive: _isActive,
companyId:
'', // Verrà ignorato dal toMap e gestito dal Cubit/SessionBloc se hai messo la logica lì
);
cubit.saveProvider(provider, _tempSelectedStoreIds);
Navigator.pop(context);
}
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(
context,
).viewInsets.bottom, // Gestisce la tastiera
left: 16,
right: 16,
top: 16,
),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
widget.initialProvider == null
? "Nuovo Provider"
: "Modifica Provider",
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
TextField(
controller: _nameController,
keyboardType: TextInputType.name,
decoration: const InputDecoration(
labelText: "Nome Gestore/Brand",
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
const Text(
"Servizi Abilitati",
style: TextStyle(fontWeight: FontWeight.bold),
),
_buildSwitch(
"Energia (Luce/Gas)",
_energy,
(v) => setState(() => _energy = v),
),
_buildSwitch(
"Telefonia Fissa",
_landline,
(v) => setState(() => _landline = v),
),
_buildSwitch(
"Telefonia Mobile",
_mobile,
(v) => setState(() => _mobile = v),
),
_buildSwitch(
"Assicurazioni",
_insurance,
(v) => setState(() => _insurance = v),
),
_buildSwitch(
"Intrattenimento",
_entertainment,
(v) => setState(() => _entertainment = v),
),
_buildSwitch(
"Finanziamenti",
_financing,
(v) => setState(() => _financing = v),
),
_buildSwitch(
"Telepass",
_telepass,
(v) => setState(() => _telepass = v),
),
_buildSwitch(
"Altro/Accessori",
_other,
(v) => setState(() => _other = v),
),
const Divider(),
_buildSwitch(
"Stato Attivo",
_isActive,
(v) => setState(() => _isActive = v),
),
const Divider(),
const Text(
"Abilita nei Negozi",
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
// Qui usiamo un BlocBuilder per prendere la lista di tutti i negozi della company
BlocBuilder<StoreCubit, StoreState>(
builder: (context, storeState) {
return Column(
children: storeState.stores.map((store) {
final isAssociated = _tempSelectedStoreIds.contains(
store.id,
);
return CheckboxListTile(
title: Text(store.name),
value: isAssociated,
onChanged: (val) {
setState(() {
if (val == true) {
_tempSelectedStoreIds.add(store.id!);
} else {
_tempSelectedStoreIds.remove(store.id);
}
});
},
);
}).toList(),
);
},
),
const SizedBox(height: 24),
ElevatedButton(
style: ElevatedButton.styleFrom(
minimumSize: const Size.fromHeight(50),
),
onPressed: _save,
child: const Text("SALVA ANAGRAFICA"),
),
const SizedBox(height: 24),
],
),
),
);
}
Widget _buildSwitch(String title, bool value, Function(bool) onChanged) {
return SwitchListTile(
title: Text(title),
value: value,
onChanged: onChanged,
contentPadding: EdgeInsets.zero,
);
}
}

View File

@@ -0,0 +1,221 @@
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/routes/routes.dart';
import 'package:flux/features/master_data/providers/blocs/provider_list_cubit.dart';
import 'package:flux/features/master_data/providers/models/provider_role.dart';
import 'package:go_router/go_router.dart';
class ProviderListScreen extends StatefulWidget {
const ProviderListScreen({super.key});
@override
State<ProviderListScreen> createState() => _ProviderListScreenState();
}
class _ProviderListScreenState extends State<ProviderListScreen> {
// Filtro attivo (null = tutti)
ProviderRole? _selectedFilter;
@override
void initState() {
super.initState();
// Chiamiamo il refresh quando entriamo (il currentStore serve per caricare quelli giusti)
final storeId = context.read<SessionCubit>().state.currentStore?.id;
if (storeId != null) {
context.read<ProviderListCubit>().loadProviders(storeId);
}
}
@override
Widget build(BuildContext context) {
final isDesktop = MediaQuery.of(context).size.width > 800;
// --- COSTRUIAMO I CHIP DEI FILTRI CON I COLORI ---
final filterChipsWidgets = [
FilterChip(
label: const Text(
'Tutti',
style: TextStyle(fontWeight: FontWeight.bold),
),
selected: _selectedFilter == null,
onSelected: (val) => setState(() => _selectedFilter = null),
),
...ProviderRole.values.map((role) {
return FilterChip(
label: Text(role.displayValue),
selected: _selectedFilter == role,
// Un po' di trasparenza al colore selezionato per non accecare
selectedColor: role.color.withValues(alpha: 0.2),
checkmarkColor: role.color,
// Bordo leggermente colorato per dare un hint visuale anche da spento
side: BorderSide(color: role.color.withValues(alpha: 0.3)),
onSelected: (val) {
setState(() => _selectedFilter = val ? role : null);
},
);
}),
];
return Scaffold(
appBar: AppBar(title: const Text('Gestione Fornitori')),
floatingActionButton: FloatingActionButton.extended(
onPressed: () => context.pushNamed(Routes.providerForm),
icon: const Icon(Icons.add),
label: const Text('Nuovo Fornitore'),
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// --- BARRA DEI FILTRI INTELLIGENTE ---
if (isDesktop)
// Desktop: Wrap multilinea con un bel padding
Padding(
padding: const EdgeInsets.all(16.0),
child: Wrap(
spacing: 8.0,
runSpacing: 8.0,
children: filterChipsWidgets,
),
)
else
// Mobile: Scorrimento orizzontale compatto
SizedBox(
height: 60,
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
itemCount: filterChipsWidgets.length,
separatorBuilder: (_, __) => const SizedBox(width: 8),
itemBuilder: (context, index) => filterChipsWidgets[index],
),
),
const Divider(height: 1),
// --- LISTA FORNITORI ---
Expanded(
child: BlocBuilder<ProviderListCubit, ProviderListState>(
builder: (context, state) {
if (state.status == ProviderListStatus.loading) {
return const Center(child: CircularProgressIndicator());
}
if (state.status == ProviderListStatus.failure) {
return Center(
child: Text(
'Errore: ${state.errorMessage}',
style: const TextStyle(color: Colors.red),
),
);
}
final displayList = _selectedFilter == null
? state.providers
: state.providers
.where((p) => p.roles.contains(_selectedFilter))
.toList();
if (displayList.isEmpty) {
return const Center(child: Text('Nessun fornitore trovato.'));
}
return ListView.separated(
itemCount: displayList.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, index) {
final provider = displayList[index];
// --- I CHIP COLORATI DELLA LISTA ---
final roleChips = Wrap(
spacing: 4,
runSpacing: 4,
children: provider.roles
.map(
(r) => Chip(
label: Text(
r.displayValue,
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w600,
color: r.color.withValues(
alpha: 0.9,
), // Testo colorato!
),
),
backgroundColor: r.color.withValues(
alpha: 0.1,
), // Sfondo pastello
side: BorderSide(
color: r.color.withValues(alpha: 0.2),
),
visualDensity: VisualDensity.compact,
padding: EdgeInsets.zero,
),
)
.toList(),
);
return ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
title: Row(
children: [
Text(
provider.name,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: provider.isActive ? null : Colors.grey,
decoration: provider.isActive
? null
: TextDecoration.lineThrough,
),
),
if (isDesktop) ...[
const SizedBox(width: 16),
Expanded(child: roleChips),
],
],
),
subtitle: isDesktop
? null
: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: roleChips,
),
trailing: const Icon(Icons.chevron_right),
onTap: () async {
await context.pushNamed(
Routes.providerForm,
extra: provider,
);
if (context.mounted) {
final storeId = context
.read<SessionCubit>()
.state
.currentStore
?.id;
if (storeId != null) {
context.read<ProviderListCubit>().loadProviders(
storeId,
);
}
}
},
);
},
);
},
),
),
],
),
);
}
}

View File

@@ -0,0 +1,132 @@
import 'package:flutter/material.dart';
import '../models/provider_location_model.dart';
class ProviderLocationDialog extends StatefulWidget {
final ProviderLocationModel? initialLocation;
const ProviderLocationDialog({super.key, this.initialLocation});
@override
State<ProviderLocationDialog> createState() => _ProviderLocationDialogState();
}
class _ProviderLocationDialogState extends State<ProviderLocationDialog> {
final _formKey = GlobalKey<FormState>();
late final TextEditingController _nameCtrl;
late final TextEditingController _addressCtrl;
late final TextEditingController _cityCtrl;
late final TextEditingController _zipCtrl;
late final TextEditingController _provCtrl;
late final TextEditingController _contactCtrl;
bool _isMain = false;
@override
void initState() {
super.initState();
final l = widget.initialLocation;
_nameCtrl = TextEditingController(text: l?.name);
_addressCtrl = TextEditingController(text: l?.address);
_cityCtrl = TextEditingController(text: l?.city);
_zipCtrl = TextEditingController(text: l?.zipCode);
_provCtrl = TextEditingController(text: l?.province);
_contactCtrl = TextEditingController(text: l?.contactPerson);
_isMain = l?.isMain ?? false;
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(
widget.initialLocation == null
? 'Aggiungi Sede/Laboratorio'
: 'Modifica Sede',
),
content: SingleChildScrollView(
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
controller: _nameCtrl,
decoration: const InputDecoration(
labelText: 'Nome Sede (es. Laboratorio Sud) *',
),
validator: (v) => v!.isEmpty ? 'Obbligatorio' : null,
),
TextFormField(
controller: _addressCtrl,
decoration: const InputDecoration(labelText: 'Indirizzo *'),
validator: (v) => v!.isEmpty ? 'Obbligatorio' : null,
),
Row(
children: [
Expanded(
flex: 2,
child: TextFormField(
controller: _cityCtrl,
decoration: const InputDecoration(labelText: 'Città *'),
validator: (v) => v!.isEmpty ? 'Obbligatorio' : null,
),
),
const SizedBox(width: 8),
Expanded(
child: TextFormField(
controller: _provCtrl,
decoration: const InputDecoration(labelText: 'Prov.'),
maxLength: 2,
),
),
],
),
TextFormField(
controller: _zipCtrl,
decoration: const InputDecoration(labelText: 'CAP *'),
keyboardType: TextInputType.number,
validator: (v) => v!.isEmpty ? 'Obbligatorio' : null,
),
TextFormField(
controller: _contactCtrl,
decoration: const InputDecoration(
labelText: 'Referente (opzionale)',
),
),
SwitchListTile(
title: const Text('Sede Principale'),
value: _isMain,
onChanged: (v) => setState(() => _isMain = v),
),
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Annulla'),
),
FilledButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
ProviderLocationModel newLocation = ProviderLocationModel(
id: widget.initialLocation?.id,
name: _nameCtrl.text.trim(),
address: _addressCtrl.text.trim(),
city: _cityCtrl.text.trim(),
zipCode: _zipCtrl.text.trim(),
province: _provCtrl.text.trim().toUpperCase(),
contactPerson: _contactCtrl.text.trim(),
isMain: _isMain,
companyId: widget.initialLocation?.companyId ?? '',
providerId: widget.initialLocation?.providerId ?? '',
);
// Restituiamo una mappa o un modello parziale (senza ID e FK che gestirà il Cubit)
Navigator.pop(context, newLocation);
}
},
child: const Text('Conferma'),
),
],
);
}
}

View File

@@ -1,180 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/features/master_data/providers/blocs/provider_cubit.dart';
import 'package:flux/features/master_data/providers/models/provider_model.dart';
import 'package:flux/features/master_data/providers/ui/provider_form_sheet.dart';
import 'package:flux/features/master_data/store/bloc/store_cubit.dart';
class ProvidersMasterDataScreen extends StatefulWidget {
const ProvidersMasterDataScreen({super.key});
@override
State<ProvidersMasterDataScreen> createState() =>
_ProvidersMasterDataScreenState();
}
class _ProvidersMasterDataScreenState extends State<ProvidersMasterDataScreen> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Anagrafica Provider")),
body: BlocBuilder<ProvidersCubit, ProvidersState>(
builder: (context, state) {
if (state.isLoading && state.allProviders.isEmpty) {
return const Center(child: CircularProgressIndicator());
}
if (state.allProviders.isEmpty) {
return Center(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Un'icona grande e stilizzata
Icon(
Icons.handshake_outlined,
size: 80,
color: Colors.indigo.withValues(alpha: 0.3),
),
const SizedBox(height: 24),
const Text(
"Nessun Provider configurato",
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
const Text(
"Aggiungi i partner con cui collabori (es. Enel, WindTre, ecc.) per poter gestire i servizi e i mandati nei tuoi negozi.",
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey),
),
const SizedBox(height: 32),
// Un bel bottone centrato per chi non vuole usare il FAB in basso
ElevatedButton.icon(
onPressed: () => _showProviderForm(context, null),
icon: const Icon(Icons.add),
label: const Text("AGGIUNGI IL PRIMO PROVIDER"),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.indigo,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
),
),
],
),
),
);
}
return ListView.separated(
itemCount: state.allProviders.length,
separatorBuilder: (context, index) => const Divider(height: 1),
itemBuilder: (context, index) {
final provider = state.allProviders[index];
return ListTile(
leading: CircleAvatar(
backgroundColor: provider.isActive
? Colors.green.shade100
: Colors.grey.shade300,
child: Icon(
Icons.business,
color: provider.isActive ? Colors.green : Colors.grey,
),
),
title: Text(
provider.name,
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: _buildCardSubtitle(
provider,
), // Una funzione che costruisce il sottotitolo con i badge
trailing: const Icon(Icons.edit_outlined),
onTap: () => _showProviderForm(context, provider),
);
},
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => _showProviderForm(context, null),
child: const Icon(Icons.add),
),
);
}
Widget _buildCardSubtitle(ProviderModel provider) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildProviderBadges(provider), // I badge che abbiamo fatto prima
const SizedBox(height: 4),
BlocBuilder<ProvidersCubit, ProvidersState>(
builder: (context, state) {
// Un piccolo testo che indica il numero di store associati
// Nota: Dovrai assicurarti che il Cubit carichi queste info
return Text(
"Disponibile in ${provider.associatedStores.length} negozi",
style: TextStyle(
fontSize: 11,
color: Colors.indigo.withValues(alpha: 0.7),
),
);
},
),
],
);
}
// Visualizza i servizi abilitati per quel provider nella lista
Widget _buildProviderBadges(ProviderModel p) {
return Wrap(
spacing: 4,
children: [
if (p.landline || p.mobile) _smallTag("📞 Tel", Colors.blue),
if (p.energy) _smallTag("⚡ Energy", Colors.orange),
if (p.insurance) _smallTag("🛡️ Assic", Colors.teal),
if (p.entertainment) _smallTag("📺 Ent", Colors.red),
if (p.financing) _smallTag("💰 Fin", Colors.purple),
if (p.telepass) _smallTag("🏎️ Telepass", Colors.yellow),
if (p.other) _smallTag("📦 Altro", Colors.grey),
],
);
}
Widget _smallTag(String label, Color color) {
return Text(
label,
style: TextStyle(color: color, fontSize: 10, fontWeight: FontWeight.w600),
);
}
// DIALOG PER INSERIMENTO/MODIFICA
void _showProviderForm(BuildContext context, ProviderModel? provider) {
final providersCubit = context.read<ProvidersCubit>();
final storeCubit = context.read<StoreCubit>();
// Implementeremo qui il form con i vari SwitchListTile
// Per ora facciamo un segnaposto o passiamo a scriverlo seriamente
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (modalContext) => MultiBlocProvider(
providers: [
BlocProvider.value(value: providersCubit),
BlocProvider.value(value: storeCubit),
],
child: ProviderFormSheet(initialProvider: provider),
),
);
}
}

View File

@@ -1,7 +1,7 @@
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/theme/theme.dart'; import 'package:flux/core/theme/theme.dart';
import 'package:flux/features/master_data/providers/blocs/provider_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.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/staff/models/staff_member_model.dart'; import 'package:flux/features/master_data/staff/models/staff_member_model.dart';
@@ -177,7 +177,7 @@ class _StoreCardState extends State<StoreCard> {
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
builder: (context) => BlocBuilder<ProvidersCubit, ProvidersState>( builder: (context) => BlocBuilder<ProviderListCubit, ProviderListState>(
builder: (context, state) { builder: (context, state) {
return Container( return Container(
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(24),

View File

@@ -515,7 +515,7 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
return Column( return Column(
children: [ children: [
_buildSectionTitle('Dettagli Servizio'), _buildSectionTitle('Dettagli Servizio'),
DetailsSection( OperationDetailsSection(
currentOp: state.operation, currentOp: state.operation,
currentType: state.operation.type, currentType: state.operation.type,
freeTextSubtypeController: _freeTextSubtypeController, freeTextSubtypeController: _freeTextSubtypeController,

View File

@@ -1,18 +1,20 @@
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/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_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_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';
class DetailsSection extends StatelessWidget { class OperationDetailsSection extends StatelessWidget {
final OperationModel? currentOp; final OperationModel? currentOp;
final String currentType; final String currentType;
final TextEditingController freeTextSubtypeController; final TextEditingController freeTextSubtypeController;
final TextEditingController freeTextDescriptionController; final TextEditingController freeTextDescriptionController;
final Widget durationQuickPicks; final Widget durationQuickPicks;
const DetailsSection({ const OperationDetailsSection({
super.key, super.key,
required this.currentOp, required this.currentOp,
required this.currentType, required this.currentType,
@@ -21,24 +23,29 @@ class DetailsSection extends StatelessWidget {
required this.durationQuickPicks, required this.durationQuickPicks,
}); });
bool _doesProviderMatchOperationType(dynamic provider, String operationType) { bool _doesProviderMatchOperationType(
ProviderModel provider,
String operationType,
) {
if (operationType == 'Custom') return true; if (operationType == 'Custom') return true;
// Controlliamo che il fornitore abbia il ruolo specifico nel suo array
switch (operationType) { switch (operationType) {
case 'AL': case 'AL' || 'MNP':
case 'MNP': return provider.roles.contains(ProviderRole.mobile);
return provider.mobile == true;
case 'NIP': case 'NIP':
return provider.landline == true; return provider.roles.contains(ProviderRole.landline);
case 'UNICA': case 'UNICA':
return provider.landline == true || provider.mobile == true; return provider.roles.contains(ProviderRole.landline) ||
provider.roles.contains(ProviderRole.mobile);
case 'Energy': case 'Energy':
return provider.energy == true; return provider.roles.contains(ProviderRole.energy);
case 'Fin': case 'Fin':
return provider.financing == true; return provider.roles.contains(ProviderRole.financing);
case 'Entertainment': case 'Entertainment':
return provider.entertainment == true; return provider.roles.contains(ProviderRole.entertainment);
case 'TELEPASS': case 'TELEPASS':
return provider.telepass == true; return provider.roles.contains(ProviderRole.telepass);
default: default:
return true; return true;
} }
@@ -79,21 +86,21 @@ class DetailsSection extends StatelessWidget {
), ),
const Divider(), const Divider(),
Expanded( Expanded(
child: BlocBuilder<ProvidersCubit, ProvidersState>( child: BlocBuilder<ProviderListCubit, ProviderListState>(
builder: (context, state) { builder: (context, state) {
if (state.isLoading) { if (state.status == ProviderListStatus.loading) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
final allProviders = state.activeProviders; // Prendiamo i provider e li filtriamo per ruolo e per stato attivo
final filteredProviders = allProviders final filteredProviders = state.providers.where((p) {
.where( final isMatch = _doesProviderMatchOperationType(
(p) => _doesProviderMatchOperationType( p,
p, operationType,
operationType, );
), return isMatch &&
) p.isActive; // Mostriamo solo quelli attivi!
.toList(); }).toList();
if (filteredProviders.isEmpty) { if (filteredProviders.isEmpty) {
return const Center( return const Center(
@@ -119,6 +126,15 @@ class DetailsSection extends StatelessWidget {
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
// Facoltativo: mostra i ruoli sotto al nome
subtitle: Text(
provider.roles
.map((r) => r.displayValue)
.join(', '),
style: const TextStyle(fontSize: 12),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
onTap: () { onTap: () {
context.read<OperationFormCubit>().updateFields( context.read<OperationFormCubit>().updateFields(
providerId: provider.id, providerId: provider.id,

View File

@@ -7,6 +7,7 @@ import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flux/features/attachments/data/attachments_repository.dart'; import 'package:flux/features/attachments/data/attachments_repository.dart';
import 'package:flux/features/auth/bloc/auth_cubit.dart'; import 'package:flux/features/auth/bloc/auth_cubit.dart';
import 'package:flux/features/company/data/company_repository.dart'; import 'package:flux/features/company/data/company_repository.dart';
import 'package:flux/features/master_data/providers/blocs/provider_list_cubit.dart';
import 'package:flux/features/operations/blocs/operation_list_cubit.dart'; import 'package:flux/features/operations/blocs/operation_list_cubit.dart';
import 'package:flux/features/operations/data/operations_repository.dart'; import 'package:flux/features/operations/data/operations_repository.dart';
import 'package:flux/features/settings/blocs/settings_cubit.dart'; import 'package:flux/features/settings/blocs/settings_cubit.dart';
@@ -29,7 +30,6 @@ import 'package:flux/features/customers/blocs/customers_cubit.dart';
import 'package:flux/features/customers/data/customer_repository.dart'; import 'package:flux/features/customers/data/customer_repository.dart';
import 'package:flux/features/master_data/products/blocs/product_cubit.dart'; import 'package:flux/features/master_data/products/blocs/product_cubit.dart';
import 'package:flux/features/master_data/products/data/product_repository.dart'; import 'package:flux/features/master_data/products/data/product_repository.dart';
import 'package:flux/features/master_data/providers/blocs/provider_cubit.dart';
import 'package:flux/features/master_data/providers/data/provider_repository.dart'; import 'package:flux/features/master_data/providers/data/provider_repository.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/staff/data/staff_repository.dart'; import 'package:flux/features/master_data/staff/data/staff_repository.dart';
@@ -67,7 +67,7 @@ void main() async {
), ),
), ),
BlocProvider<OperationListCubit>(create: (_) => OperationListCubit()), BlocProvider<OperationListCubit>(create: (_) => OperationListCubit()),
BlocProvider<ProvidersCubit>(create: (_) => ProvidersCubit()), BlocProvider<ProviderListCubit>(create: (_) => ProviderListCubit()),
BlocProvider<SettingsCubit>(create: (_) => SettingsCubit()), BlocProvider<SettingsCubit>(create: (_) => SettingsCubit()),
BlocProvider<TicketListCubit>(create: (_) => TicketListCubit()), BlocProvider<TicketListCubit>(create: (_) => TicketListCubit()),
BlocProvider<OperationListCubit>(create: (_) => OperationListCubit()), BlocProvider<OperationListCubit>(create: (_) => OperationListCubit()),
@@ -165,7 +165,6 @@ class _FluxAppState extends State<FluxApp> {
// BAM! L'utente è dentro. Pre-carichiamo i Cubit leggeri. // BAM! L'utente è dentro. Pre-carichiamo i Cubit leggeri.
context.read<StoreCubit>().loadStores(); context.read<StoreCubit>().loadStores();
context.read<StaffCubit>().loadAllStaff(); context.read<StaffCubit>().loadAllStaff();
context.read<ProvidersCubit>().loadProviders();
}, },
// --- PARTE BUILDER (La UI che viene disegnata a schermo) --- // --- PARTE BUILDER (La UI che viene disegnata a schermo) ---