refactor providers e basi per spedizioni
This commit is contained in:
@@ -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<ProviderFormCubit>(
|
||||
// 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<CustomersCubit>().loadCustomers();
|
||||
context.read<ProvidersCubit>().loadActiveProvidersForStore(
|
||||
currentStoreId,
|
||||
);
|
||||
context.read<ProviderListCubit>().loadProviders(currentStoreId);
|
||||
context.read<ProductsCubit>().loadModels();
|
||||
context.read<ProductsCubit>().loadBrands();
|
||||
return MultiBlocProvider(
|
||||
|
||||
@@ -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';
|
||||
|
||||
64
lib/features/documents/models/shipment_document_model.dart
Normal file
64
lib/features/documents/models/shipment_document_model.dart
Normal 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];
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
@@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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<SupabaseClient>();
|
||||
|
||||
// --- ASSOCIAZIONE PROVIDER <-> STORE ---
|
||||
|
||||
// Aggiunge un provider a un negozio (Attiva mandato)
|
||||
Future<void> 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<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 {
|
||||
// 1. Carica i provider abilitati per uno specifico Store
|
||||
Future<List<ProviderModel>> getProvidersByStore(String storeId) async {
|
||||
final response = await _supabase
|
||||
.from('provider')
|
||||
.from('providers_in_stores')
|
||||
.select('''
|
||||
provider_id,
|
||||
provider:provider (
|
||||
*,
|
||||
associated_stores:providers_in_stores (
|
||||
store (
|
||||
*
|
||||
)
|
||||
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<String, dynamic>);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
// Recupera gli ID dei provider associati a uno store (utile per le checkbox)
|
||||
Future<List<String>> fetchAssociatedProviderIds(String storeId) async {
|
||||
try {
|
||||
final response = await _supabase
|
||||
.from('providers_in_stores')
|
||||
.select('provider_id')
|
||||
.eq('store_id', storeId);
|
||||
|
||||
return (response as List)
|
||||
.map((item) => item['provider_id'].toString())
|
||||
.toList();
|
||||
} catch (e) {
|
||||
throw Exception('Errore recupero ID associati: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// --- FUNZIONI STANDARD ---
|
||||
|
||||
// Questa la userai nel Form Servizi: carica solo i provider abilitati per lo store
|
||||
Future<List<ProviderModel>> fetchActiveProvidersForStore(
|
||||
String storeId,
|
||||
) async {
|
||||
try {
|
||||
// 2. Carica TUTTI i provider della Company (per la gestione anagrafica)
|
||||
Future<List<ProviderModel>> getAllCompanyProviders() async {
|
||||
final response = await _supabase
|
||||
.from('provider')
|
||||
.select('*, providers_in_stores!inner(store_id)')
|
||||
.eq('providers_in_stores.store_id', storeId)
|
||||
.eq('is_active', true);
|
||||
.select('*, provider_locations (*)')
|
||||
.order('name');
|
||||
|
||||
return (response as List).map((m) => ProviderModel.fromMap(m)).toList();
|
||||
} catch (e) {
|
||||
throw Exception('Errore fetch provider attivi: $e');
|
||||
}
|
||||
return (response as List)
|
||||
.map((row) => ProviderModel.fromMap(row as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
|
||||
// Salva o aggiorna l'anagrafica del Provider
|
||||
Future<ProviderModel> saveProvider(ProviderModel provider) async {
|
||||
try {
|
||||
// .select().single() è fondamentale per farsi restituire
|
||||
// l'oggetto appena creato/aggiornato con l'ID
|
||||
final response = await _supabase
|
||||
// 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();
|
||||
|
||||
return ProviderModel.fromMap(response); // <--- DEVE ESSERCI IL RETURN
|
||||
} catch (e) {
|
||||
rethrow; // <--- Rilancia l'errore al Cubit, non ritornare null!
|
||||
}
|
||||
}
|
||||
final savedProvider = ProviderModel.fromMap(savedRow);
|
||||
|
||||
Future<void> syncProviderStores(
|
||||
String providerId,
|
||||
List<String> storeIds,
|
||||
) async {
|
||||
try {
|
||||
// 1. Eliminiamo tutte le associazioni correnti per questo provider
|
||||
// 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', providerId);
|
||||
.eq('provider_id', savedProvider.id!);
|
||||
|
||||
// 2. Se ci sono nuovi store da associare, li inseriamo
|
||||
if (storeIds.isNotEmpty) {
|
||||
final inserts = storeIds
|
||||
.map((sId) => {'provider_id': providerId, 'store_id': sId})
|
||||
if (enabledStoreIds.isNotEmpty) {
|
||||
final storeLinks = enabledStoreIds
|
||||
.map((sId) => {'provider_id': savedProvider.id, 'store_id': sId})
|
||||
.toList();
|
||||
|
||||
await _supabase.from('providers_in_stores').insert(inserts);
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Errore durante la sincronizzazione store: $e');
|
||||
await _supabase.from('providers_in_stores').insert(storeLinks);
|
||||
}
|
||||
|
||||
return savedProvider;
|
||||
}
|
||||
|
||||
// 4. Gestione Sedi (Locations)
|
||||
Future<void> saveLocation(ProviderLocationModel location) async {
|
||||
await _supabase.from('provider_locations').upsert(location.toMap());
|
||||
}
|
||||
|
||||
Future<void> deleteLocation(String locationId) async {
|
||||
await _supabase.from('provider_locations').delete().eq('id', locationId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
@@ -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<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({
|
||||
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<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) {
|
||||
// Estraiamo la lista dalla pivot e poi prendiamo l'oggetto 'store' annidato
|
||||
final pivotList = map['associated_stores'] as List?;
|
||||
List<StoreModel> stores = [];
|
||||
if (pivotList != null) {
|
||||
stores = pivotList
|
||||
.where((item) => item['store'] != null) // Sicurezza
|
||||
// Parsing sicuro dell'array testuale di Supabase per trasformarlo in Enum
|
||||
List<ProviderRole> parsedRoles = [];
|
||||
if (map['roles'] != null) {
|
||||
final List<dynamic> 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<ProviderLocationModel>? parsedLocations;
|
||||
if (map['provider_locations'] != null) {
|
||||
parsedLocations = (map['provider_locations'] as List<dynamic>)
|
||||
.map(
|
||||
(item) => StoreModel.fromMap(item['store'] as Map<String, dynamic>),
|
||||
(item) =>
|
||||
ProviderLocationModel.fromMap(item as Map<String, dynamic>),
|
||||
)
|
||||
.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<String, dynamic> 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<Object?> 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<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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
28
lib/features/master_data/providers/models/provider_role.dart
Normal file
28
lib/features/master_data/providers/models/provider_role.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
395
lib/features/master_data/providers/ui/provider_form_screen.dart
Normal file
395
lib/features/master_data/providers/ui/provider_form_screen.dart
Normal 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),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
221
lib/features/master_data/providers/ui/provider_list_screen.dart
Normal file
221
lib/features/master_data/providers/ui/provider_list_screen.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<StoreCard> {
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
|
||||
builder: (context) => BlocBuilder<ProvidersCubit, ProvidersState>(
|
||||
builder: (context) => BlocBuilder<ProviderListCubit, ProviderListState>(
|
||||
builder: (context, state) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
|
||||
@@ -515,7 +515,7 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
|
||||
return Column(
|
||||
children: [
|
||||
_buildSectionTitle('Dettagli Servizio'),
|
||||
DetailsSection(
|
||||
OperationDetailsSection(
|
||||
currentOp: state.operation,
|
||||
currentType: state.operation.type,
|
||||
freeTextSubtypeController: _freeTextSubtypeController,
|
||||
|
||||
@@ -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<ProvidersCubit, ProvidersState>(
|
||||
child: BlocBuilder<ProviderListCubit, ProviderListState>(
|
||||
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(
|
||||
// Prendiamo i provider e li filtriamo per ruolo e per stato attivo
|
||||
final filteredProviders = state.providers.where((p) {
|
||||
final isMatch = _doesProviderMatchOperationType(
|
||||
p,
|
||||
operationType,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
);
|
||||
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<OperationFormCubit>().updateFields(
|
||||
providerId: provider.id,
|
||||
|
||||
@@ -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<OperationListCubit>(create: (_) => OperationListCubit()),
|
||||
BlocProvider<ProvidersCubit>(create: (_) => ProvidersCubit()),
|
||||
BlocProvider<ProviderListCubit>(create: (_) => ProviderListCubit()),
|
||||
BlocProvider<SettingsCubit>(create: (_) => SettingsCubit()),
|
||||
BlocProvider<TicketListCubit>(create: (_) => TicketListCubit()),
|
||||
BlocProvider<OperationListCubit>(create: (_) => OperationListCubit()),
|
||||
@@ -165,7 +165,6 @@ class _FluxAppState extends State<FluxApp> {
|
||||
// BAM! L'utente è dentro. Pre-carichiamo i Cubit leggeri.
|
||||
context.read<StoreCubit>().loadStores();
|
||||
context.read<StaffCubit>().loadAllStaff();
|
||||
context.read<ProvidersCubit>().loadProviders();
|
||||
},
|
||||
|
||||
// --- PARTE BUILDER (La UI che viene disegnata a schermo) ---
|
||||
|
||||
Reference in New Issue
Block a user