From f19f19a2795eb5b389ce255bd88e2c9e57558c33 Mon Sep 17 00:00:00 2001 From: Mark M2 Macbook Date: Fri, 15 May 2026 10:12:05 +0200 Subject: [PATCH] refactor providers e basi per spedizioni --- lib/core/routes/app_router.dart | 32 +- lib/core/routes/routes.dart | 1 + .../models/shipment_document_model.dart | 64 +++ .../providers/blocs/provider_cubit.dart | 176 -------- .../providers/blocs/provider_form_cubit.dart | 182 ++++++++ .../providers/blocs/provider_form_state.dart | 55 +++ .../providers/blocs/provider_list_cubit.dart | 53 +++ .../providers/blocs/provider_list_state.dart | 34 ++ .../providers/data/provider_repository.dart | 180 +++----- .../models/provider_location_model.dart | 109 +++++ .../providers/models/provider_model.dart | 241 ++++++----- .../providers/models/provider_role.dart | 28 ++ .../providers/ui/provider_form_screen.dart | 395 ++++++++++++++++++ .../providers/ui/provider_form_sheet.dart | 214 ---------- .../providers/ui/provider_list_screen.dart | 221 ++++++++++ .../ui/provider_location_dialog.dart | 132 ++++++ .../ui/providers_master_data_screen.dart | 180 -------- .../master_data/store/ui/store_card.dart | 4 +- .../operations/ui/operation_form_screen.dart | 2 +- .../ui/widgets/details_section.dart | 64 +-- lib/main.dart | 5 +- 21 files changed, 1542 insertions(+), 830 deletions(-) create mode 100644 lib/features/documents/models/shipment_document_model.dart delete mode 100644 lib/features/master_data/providers/blocs/provider_cubit.dart create mode 100644 lib/features/master_data/providers/blocs/provider_form_cubit.dart create mode 100644 lib/features/master_data/providers/blocs/provider_form_state.dart create mode 100644 lib/features/master_data/providers/blocs/provider_list_cubit.dart create mode 100644 lib/features/master_data/providers/blocs/provider_list_state.dart create mode 100644 lib/features/master_data/providers/models/provider_location_model.dart create mode 100644 lib/features/master_data/providers/models/provider_role.dart create mode 100644 lib/features/master_data/providers/ui/provider_form_screen.dart delete mode 100644 lib/features/master_data/providers/ui/provider_form_sheet.dart create mode 100644 lib/features/master_data/providers/ui/provider_list_screen.dart create mode 100644 lib/features/master_data/providers/ui/provider_location_dialog.dart delete mode 100644 lib/features/master_data/providers/ui/providers_master_data_screen.dart diff --git a/lib/core/routes/app_router.dart b/lib/core/routes/app_router.dart index 089699b..fcf9a0f 100644 --- a/lib/core/routes/app_router.dart +++ b/lib/core/routes/app_router.dart @@ -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/products/blocs/product_cubit.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/ui/providers_master_data_screen.dart'; +import 'package:flux/features/master_data/providers/blocs/provider_form_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/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/ui/staff_screen.dart'; import 'package:flux/features/master_data/store/ui/stores_screen.dart'; @@ -159,10 +162,9 @@ class AppRouter { builder: (context, state) => const StoresScreen(), ), GoRoute( - path: 'providers', - name: Routes.providers, // Diventa /master-data/providers - builder: (context, state) => - const ProvidersMasterDataScreen(), + path: '/providers', + name: Routes.providers, + builder: (context, state) => const ProviderListScreen(), ), ], ), @@ -200,6 +202,20 @@ class AppRouter { ), // --- 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( + // Inizializziamo un Cubit NUOVO ogni volta che apriamo il form + create: (context) => ProviderFormCubit(), + child: ProviderFormScreen(existingProvider: existingProvider), + ); + }, + ), GoRoute( // Il path sarà es. /tickets/form/123 oppure /tickets/form/new path: '/tickets/form/:id', @@ -335,9 +351,7 @@ class AppRouter { .currentStore! .id!; context.read().loadCustomers(); - context.read().loadActiveProvidersForStore( - currentStoreId, - ); + context.read().loadProviders(currentStoreId); context.read().loadModels(); context.read().loadBrands(); return MultiBlocProvider( diff --git a/lib/core/routes/routes.dart b/lib/core/routes/routes.dart index 89eff79..991823e 100644 --- a/lib/core/routes/routes.dart +++ b/lib/core/routes/routes.dart @@ -9,6 +9,7 @@ class Routes { static const String staff = 'staff'; static const String stores = 'stores'; static const String providers = 'providers'; + static const String providerForm = 'provider-form'; static const String settings = 'settings'; static const String themeSettings = 'themeSettings'; static const String operations = 'operations'; diff --git a/lib/features/documents/models/shipment_document_model.dart b/lib/features/documents/models/shipment_document_model.dart new file mode 100644 index 0000000..2c1cdba --- /dev/null +++ b/lib/features/documents/models/shipment_document_model.dart @@ -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 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 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 get props => [id, docNumber, ticketId]; +} diff --git a/lib/features/master_data/providers/blocs/provider_cubit.dart b/lib/features/master_data/providers/blocs/provider_cubit.dart deleted file mode 100644 index 52e5302..0000000 --- a/lib/features/master_data/providers/blocs/provider_cubit.dart +++ /dev/null @@ -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 allProviders; - final List associatedIds; - // NUOVO CAMPO: Lista dei provider pronti per essere usati nel form pratiche - final List 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? allProviders, - List? associatedIds, - List? 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 get props => [ - allProviders, - associatedIds, - activeProviders, // Aggiungi qui - isLoading, - errorMessage, - ]; -} - -class ProvidersCubit extends Cubit { - final ProviderRepository _repository = GetIt.I(); - final SessionCubit _sessionCubit = GetIt.I(); - - ProvidersCubit() : super(const ProvidersState()); - - // Carica i provider della company e quelli associati a uno store specifico - Future loadProviders({StoreModel? store}) async { - emit(state.copyWith(isLoading: true)); - try { - final all = await _repository.fetchAllCompanyProviders( - _sessionCubit.state.company!.id!, - ); - List 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 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 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.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.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 saveProvider( - ProviderModel provider, - List 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 saveProviderWithStores( - ProviderModel provider, - List 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())); - } - } -} diff --git a/lib/features/master_data/providers/blocs/provider_form_cubit.dart b/lib/features/master_data/providers/blocs/provider_form_cubit.dart new file mode 100644 index 0000000..d5dcc84 --- /dev/null +++ b/lib/features/master_data/providers/blocs/provider_form_cubit.dart @@ -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 { + final ProviderRepository _repository = GetIt.I.get(); + final _client = Supabase.instance.client; // Lo usiamo al volo per gli store + + ProviderFormCubit() + : super(ProviderFormState(provider: ProviderModel.empty(companyId: ''))); + + // --- INIZIALIZZAZIONE --- + Future 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 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, + 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.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.from(state.selectedStoreIds); + if (currentStoreIds.contains(storeId)) { + currentStoreIds.remove(storeId); + } else { + currentStoreIds.add(storeId); + } + emit(state.copyWith(selectedStoreIds: currentStoreIds)); + } + + Future addLocationLocal(ProviderLocationModel location) async { + final currentLocations = List.from( + state.localLocations, + ); + currentLocations.add(location); + emit(state.copyWith(localLocations: currentLocations)); + } + + void removeLocationLocal(int index) { + final currentLocations = List.from( + state.localLocations, + ); + if (index >= 0 && index < currentLocations.length) { + currentLocations.removeAt(index); + emit(state.copyWith(localLocations: currentLocations)); + } + } + + // --- SALVATAGGIO FINALE --- + Future 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', + ), + ); + } + } +} diff --git a/lib/features/master_data/providers/blocs/provider_form_state.dart b/lib/features/master_data/providers/blocs/provider_form_state.dart new file mode 100644 index 0000000..7edc4f0 --- /dev/null +++ b/lib/features/master_data/providers/blocs/provider_form_state.dart @@ -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 + availableStores; // Metti List se hai il modello + final List selectedStoreIds; // IDs dei negozi in cui è attivo + final List + 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? availableStores, + List? selectedStoreIds, + List? 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 get props => [ + status, + provider, + availableStores, + selectedStoreIds, + localLocations, + errorMessage, + ]; +} diff --git a/lib/features/master_data/providers/blocs/provider_list_cubit.dart b/lib/features/master_data/providers/blocs/provider_list_cubit.dart new file mode 100644 index 0000000..8aced80 --- /dev/null +++ b/lib/features/master_data/providers/blocs/provider_list_cubit.dart @@ -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 { + final ProviderRepository _repository = GetIt.I.get(); + + ProviderListCubit() : super(const ProviderListState()); + + Future 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 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(), + ), + ); + } + } +} diff --git a/lib/features/master_data/providers/blocs/provider_list_state.dart b/lib/features/master_data/providers/blocs/provider_list_state.dart new file mode 100644 index 0000000..8c6d1c3 --- /dev/null +++ b/lib/features/master_data/providers/blocs/provider_list_state.dart @@ -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 providers; + final List allProviders; + final String? errorMessage; + + const ProviderListState({ + this.status = ProviderListStatus.initial, + this.providers = const [], + this.allProviders = const [], + this.errorMessage, + }); + + ProviderListState copyWith({ + ProviderListStatus? status, + List? providers, + List? allProviders, + String? errorMessage, + }) { + return ProviderListState( + status: status ?? this.status, + providers: providers ?? this.providers, + allProviders: allProviders ?? this.allProviders, + errorMessage: errorMessage ?? this.errorMessage, + ); + } + + @override + List get props => [status, providers, allProviders, errorMessage]; +} diff --git a/lib/features/master_data/providers/data/provider_repository.dart b/lib/features/master_data/providers/data/provider_repository.dart index 54b377d..4101378 100644 --- a/lib/features/master_data/providers/data/provider_repository.dart +++ b/lib/features/master_data/providers/data/provider_repository.dart @@ -1,137 +1,81 @@ +import 'package:get_it/get_it.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import '../models/provider_model.dart'; +import '../models/provider_location_model.dart'; class ProviderRepository { - final _supabase = Supabase.instance.client; + final _supabase = GetIt.I.get(); - // --- ASSOCIAZIONE PROVIDER <-> STORE --- - - // Aggiunge un provider a un negozio (Attiva mandato) - Future associateProviderToStore({ - required String providerId, - required String storeId, - }) async { - try { - await _supabase.from('providers_in_stores').insert({ - '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 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> fetchAllCompanyProviders(String companyId) async { - try { - final response = await _supabase - .from('provider') - .select(''' - *, - associated_stores:providers_in_stores ( - store ( - * - ) + // 1. Carica i provider abilitati per uno specifico Store + Future> getProvidersByStore(String storeId) async { + final response = await _supabase + .from('providers_in_stores') + .select(''' + provider_id, + provider:provider ( + *, + provider_locations (*) ) ''') - .eq('company_id', companyId) - .order('name'); + .eq('store_id', storeId) + .order('name', referencedTable: 'provider'); - return (response as List).map((m) => ProviderModel.fromMap(m)).toList(); - } catch (e) { - throw 'Errore fetch providers: $e'; - } + // Mappiamo i risultati estraendo l'oggetto 'provider' annidato + return (response as List).map((row) { + return ProviderModel.fromMap(row['provider'] as Map); + }).toList(); } - // Recupera gli ID dei provider associati a uno store (utile per le checkbox) - Future> fetchAssociatedProviderIds(String storeId) async { - try { - final response = await _supabase - .from('providers_in_stores') - .select('provider_id') - .eq('store_id', storeId); + // 2. Carica TUTTI i provider della Company (per la gestione anagrafica) + Future> getAllCompanyProviders() async { + final response = await _supabase + .from('provider') + .select('*, provider_locations (*)') + .order('name'); - return (response as List) - .map((item) => item['provider_id'].toString()) + return (response as List) + .map((row) => ProviderModel.fromMap(row as Map)) + .toList(); + } + + // 3. Salvataggio atomico (Upsert) del Provider + Future saveProvider( + ProviderModel provider, + List 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(); - } catch (e) { - throw Exception('Errore recupero ID associati: $e'); + + await _supabase.from('providers_in_stores').insert(storeLinks); } + + return savedProvider; } - // --- FUNZIONI STANDARD --- - - // Questa la userai nel Form Servizi: carica solo i provider abilitati per lo store - Future> 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'); - } + // 4. Gestione Sedi (Locations) + Future saveLocation(ProviderLocationModel location) async { + await _supabase.from('provider_locations').upsert(location.toMap()); } - // Salva o aggiorna l'anagrafica del Provider - Future saveProvider(ProviderModel provider) async { - 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 syncProviderStores( - String providerId, - List 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'); - } + Future deleteLocation(String locationId) async { + await _supabase.from('provider_locations').delete().eq('id', locationId); } } diff --git a/lib/features/master_data/providers/models/provider_location_model.dart b/lib/features/master_data/providers/models/provider_location_model.dart new file mode 100644 index 0000000..1bad099 --- /dev/null +++ b/lib/features/master_data/providers/models/provider_location_model.dart @@ -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 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 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 get props => [ + id, + providerId, + companyId, + name, + address, + city, + zipCode, + province, + contactPerson, + isMain, + ]; +} diff --git a/lib/features/master_data/providers/models/provider_model.dart b/lib/features/master_data/providers/models/provider_model.dart index 5027f30..a1c0bd6 100644 --- a/lib/features/master_data/providers/models/provider_model.dart +++ b/lib/features/master_data/providers/models/provider_model.dart @@ -1,134 +1,169 @@ 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 { 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 List 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 roles; + final List? locations; const ProviderModel({ 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, - 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? roles, + List? 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 map) { - // Estraiamo la lista dalla pivot e poi prendiamo l'oggetto 'store' annidato - final pivotList = map['associated_stores'] as List?; - List stores = []; - if (pivotList != null) { - stores = pivotList - .where((item) => item['store'] != null) // Sicurezza + // Parsing sicuro dell'array testuale di Supabase per trasformarlo in Enum + List parsedRoles = []; + if (map['roles'] != null) { + final List rawRoles = map['roles']; + for (var r in rawRoles) { + final role = ProviderRole.fromString(r as String); + if (role != null) parsedRoles.add(role); + } + } + + // Parsing della JOIN per le locations, se presenti nella query + List? parsedLocations; + if (map['provider_locations'] != null) { + parsedLocations = (map['provider_locations'] as List) .map( - (item) => StoreModel.fromMap(item['store'] as Map), + (item) => + ProviderLocationModel.fromMap(item as Map), ) .toList(); } + return ProviderModel( - id: map['id'], - name: map['name'], - landline: map['landline'] ?? false, - mobile: map['mobile'] ?? false, - energy: map['energy'] ?? false, - insurance: map['insurance'] ?? false, - entertainment: map['entertainment'] ?? false, - financing: map['financing'] ?? false, - telepass: map['telepass'] ?? false, - other: map['other'] ?? false, - isActive: map['is_active'] ?? true, - companyId: map['company_id'], - associatedStores: stores, + id: map['id'] as String, + companyId: map['company_id'] as String, + name: map['name'] as String, + isActive: map['is_active'] as bool? ?? true, + businessName: map['business_name'] as String?, + vatNumber: map['vat_number'] as String?, + fiscalCode: map['fiscal_code'] as String?, + sdiCode: map['sdi_code'] as String?, + emailPec: map['email_pec'] as String?, + legalAddress: map['legal_address'] as String?, + legalCity: map['legal_city'] as String?, + legalZip: map['legal_zip'] as String?, + legalProvince: map['legal_province'] as String?, + roles: parsedRoles, + locations: parsedLocations, ); } Map toMap() { - final map = { - 'name': name, - 'landline': landline, - 'mobile': mobile, - 'energy': energy, - 'insurance': insurance, - 'entertainment': entertainment, - 'financing': financing, - 'telepass': telepass, - 'other': other, - 'is_active': isActive, + return { + if (id != null) 'id': id, '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 List get props => [ id, - name, - landline, - mobile, - energy, - insurance, - entertainment, - financing, - telepass, - other, - isActive, 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? 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, - ); - } } diff --git a/lib/features/master_data/providers/models/provider_role.dart b/lib/features/master_data/providers/models/provider_role.dart new file mode 100644 index 0000000..521473c --- /dev/null +++ b/lib/features/master_data/providers/models/provider_role.dart @@ -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; + } + } +} diff --git a/lib/features/master_data/providers/ui/provider_form_screen.dart b/lib/features/master_data/providers/ui/provider_form_screen.dart new file mode 100644 index 0000000..39171c4 --- /dev/null +++ b/lib/features/master_data/providers/ui/provider_form_screen.dart @@ -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 createState() => _ProviderFormScreenState(); +} + +class _ProviderFormScreenState extends State { + final _formKey = GlobalKey(); + + // 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() + .state + .currentStore! + .companyId; + + context.read().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().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( + 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().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().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().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(); + final res = await showDialog( + 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() + .removeLocationLocal(index), + ), + ); + }, + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/master_data/providers/ui/provider_form_sheet.dart b/lib/features/master_data/providers/ui/provider_form_sheet.dart deleted file mode 100644 index 0fead8a..0000000 --- a/lib/features/master_data/providers/ui/provider_form_sheet.dart +++ /dev/null @@ -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 createState() => _ProviderFormSheetState(); -} - -class _ProviderFormSheetState extends State { - 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 _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(); - 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( - 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, - ); - } -} diff --git a/lib/features/master_data/providers/ui/provider_list_screen.dart b/lib/features/master_data/providers/ui/provider_list_screen.dart new file mode 100644 index 0000000..0b96973 --- /dev/null +++ b/lib/features/master_data/providers/ui/provider_list_screen.dart @@ -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 createState() => _ProviderListScreenState(); +} + +class _ProviderListScreenState extends State { + // 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().state.currentStore?.id; + if (storeId != null) { + context.read().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( + 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() + .state + .currentStore + ?.id; + if (storeId != null) { + context.read().loadProviders( + storeId, + ); + } + } + }, + ); + }, + ); + }, + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/master_data/providers/ui/provider_location_dialog.dart b/lib/features/master_data/providers/ui/provider_location_dialog.dart new file mode 100644 index 0000000..9e828d1 --- /dev/null +++ b/lib/features/master_data/providers/ui/provider_location_dialog.dart @@ -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 createState() => _ProviderLocationDialogState(); +} + +class _ProviderLocationDialogState extends State { + final _formKey = GlobalKey(); + 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'), + ), + ], + ); + } +} diff --git a/lib/features/master_data/providers/ui/providers_master_data_screen.dart b/lib/features/master_data/providers/ui/providers_master_data_screen.dart deleted file mode 100644 index 465f4da..0000000 --- a/lib/features/master_data/providers/ui/providers_master_data_screen.dart +++ /dev/null @@ -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 createState() => - _ProvidersMasterDataScreenState(); -} - -class _ProvidersMasterDataScreenState extends State { - @override - void initState() { - super.initState(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text("Anagrafica Provider")), - body: BlocBuilder( - 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( - 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(); - final storeCubit = context.read(); - // 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), - ), - ); - } -} diff --git a/lib/features/master_data/store/ui/store_card.dart b/lib/features/master_data/store/ui/store_card.dart index 1c2d8b4..1abf210 100644 --- a/lib/features/master_data/store/ui/store_card.dart +++ b/lib/features/master_data/store/ui/store_card.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.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/staff/blocs/staff_cubit.dart'; import 'package:flux/features/master_data/staff/models/staff_member_model.dart'; @@ -177,7 +177,7 @@ class _StoreCardState extends State { context: context, isScrollControlled: true, - builder: (context) => BlocBuilder( + builder: (context) => BlocBuilder( builder: (context, state) { return Container( padding: const EdgeInsets.all(24), diff --git a/lib/features/operations/ui/operation_form_screen.dart b/lib/features/operations/ui/operation_form_screen.dart index 718e3c0..3556110 100644 --- a/lib/features/operations/ui/operation_form_screen.dart +++ b/lib/features/operations/ui/operation_form_screen.dart @@ -515,7 +515,7 @@ class _OperationFormScreenState extends State { return Column( children: [ _buildSectionTitle('Dettagli Servizio'), - DetailsSection( + OperationDetailsSection( currentOp: state.operation, currentType: state.operation.type, freeTextSubtypeController: _freeTextSubtypeController, diff --git a/lib/features/operations/ui/widgets/details_section.dart b/lib/features/operations/ui/widgets/details_section.dart index b64f388..5866ff7 100644 --- a/lib/features/operations/ui/widgets/details_section.dart +++ b/lib/features/operations/ui/widgets/details_section.dart @@ -1,18 +1,20 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.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/models/operation_model.dart'; -class DetailsSection extends StatelessWidget { +class OperationDetailsSection extends StatelessWidget { final OperationModel? currentOp; final String currentType; final TextEditingController freeTextSubtypeController; final TextEditingController freeTextDescriptionController; final Widget durationQuickPicks; - const DetailsSection({ + const OperationDetailsSection({ super.key, required this.currentOp, required this.currentType, @@ -21,24 +23,29 @@ class DetailsSection extends StatelessWidget { required this.durationQuickPicks, }); - bool _doesProviderMatchOperationType(dynamic provider, String operationType) { + bool _doesProviderMatchOperationType( + ProviderModel provider, + String operationType, + ) { if (operationType == 'Custom') return true; + + // Controlliamo che il fornitore abbia il ruolo specifico nel suo array switch (operationType) { - case 'AL': - case 'MNP': - return provider.mobile == true; + case 'AL' || 'MNP': + return provider.roles.contains(ProviderRole.mobile); case 'NIP': - return provider.landline == true; + return provider.roles.contains(ProviderRole.landline); case 'UNICA': - return provider.landline == true || provider.mobile == true; + return provider.roles.contains(ProviderRole.landline) || + provider.roles.contains(ProviderRole.mobile); case 'Energy': - return provider.energy == true; + return provider.roles.contains(ProviderRole.energy); case 'Fin': - return provider.financing == true; + return provider.roles.contains(ProviderRole.financing); case 'Entertainment': - return provider.entertainment == true; + return provider.roles.contains(ProviderRole.entertainment); case 'TELEPASS': - return provider.telepass == true; + return provider.roles.contains(ProviderRole.telepass); default: return true; } @@ -79,21 +86,21 @@ class DetailsSection extends StatelessWidget { ), const Divider(), Expanded( - child: BlocBuilder( + child: BlocBuilder( builder: (context, state) { - if (state.isLoading) { + if (state.status == ProviderListStatus.loading) { return const Center(child: CircularProgressIndicator()); } - final allProviders = state.activeProviders; - final filteredProviders = allProviders - .where( - (p) => _doesProviderMatchOperationType( - p, - operationType, - ), - ) - .toList(); + // Prendiamo i provider e li filtriamo per ruolo e per stato attivo + final filteredProviders = state.providers.where((p) { + final isMatch = _doesProviderMatchOperationType( + p, + operationType, + ); + return isMatch && + p.isActive; // Mostriamo solo quelli attivi! + }).toList(); if (filteredProviders.isEmpty) { return const Center( @@ -119,6 +126,15 @@ class DetailsSection extends StatelessWidget { 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: () { context.read().updateFields( providerId: provider.id, diff --git a/lib/main.dart b/lib/main.dart index d34325f..3ebdbf2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,6 +7,7 @@ import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flux/features/attachments/data/attachments_repository.dart'; import 'package:flux/features/auth/bloc/auth_cubit.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/data/operations_repository.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/master_data/products/blocs/product_cubit.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/staff/blocs/staff_cubit.dart'; import 'package:flux/features/master_data/staff/data/staff_repository.dart'; @@ -67,7 +67,7 @@ void main() async { ), ), BlocProvider(create: (_) => OperationListCubit()), - BlocProvider(create: (_) => ProvidersCubit()), + BlocProvider(create: (_) => ProviderListCubit()), BlocProvider(create: (_) => SettingsCubit()), BlocProvider(create: (_) => TicketListCubit()), BlocProvider(create: (_) => OperationListCubit()), @@ -165,7 +165,6 @@ class _FluxAppState extends State { // BAM! L'utente è dentro. Pre-carichiamo i Cubit leggeri. context.read().loadStores(); context.read().loadAllStaff(); - context.read().loadProviders(); }, // --- PARTE BUILDER (La UI che viene disegnata a schermo) ---