From 4580173edff33357825f502da7a8db1a3c883c7b Mon Sep 17 00:00:00 2001 From: Mark M2 Macbook Date: Sun, 3 May 2026 10:08:57 +0200 Subject: [PATCH] new operation form almost ready Co-authored-by: Copilot --- lib/core/routes/app_router.dart | 15 +- .../customers/data/customer_repository.dart | 2 +- .../products/blocs/product_cubit.dart | 29 +- .../products/data/product_repository.dart | 19 +- .../products/ui/brand_selector.dart | 2 +- .../master_data/products/ui/models_list.dart | 2 +- .../products/ui/product_dialogs.dart | 4 +- .../products/ui/products_screen.dart | 4 +- .../products/ui/quick_product_dialog.dart | 2 +- .../providers/models/provider_model.dart | 7 + .../providers/ui/provider_form_sheet.dart | 8 + .../ui/providers_master_data_screen.dart | 2 + .../operations/blocs/operations_cubit.dart | 49 +- .../operations/ui/operation_form_screen.dart | 492 ++++++++++++++++-- lib/main.dart | 2 +- 15 files changed, 578 insertions(+), 61 deletions(-) diff --git a/lib/core/routes/app_router.dart b/lib/core/routes/app_router.dart index c577ed2..1ea8bba 100644 --- a/lib/core/routes/app_router.dart +++ b/lib/core/routes/app_router.dart @@ -8,13 +8,16 @@ import 'package:flux/core/utils/extensions.dart'; import 'package:flux/core/widgets/set_password_screen.dart'; import 'package:flux/features/auth/ui/auth_screen.dart'; import 'package:flux/features/customers/blocs/customer_files_bloc.dart'; +import 'package:flux/features/customers/blocs/customers_cubit.dart'; import 'package:flux/features/customers/models/customer_model.dart'; import 'package:flux/features/customers/ui/customer_detail_screen.dart'; import 'package:flux/features/customers/ui/customer_mobile_upload_screen.dart'; import 'package:flux/features/customers/ui/customers_content.dart'; 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/staff/ui/staff_screen.dart'; import 'package:flux/features/master_data/store/ui/stores_screen.dart'; @@ -97,7 +100,11 @@ class AppRouter { routes: [ GoRoute( path: 'products', // Diventa /master-data/products - builder: (context, state) => const ProductsScreen(), + builder: (context, state) { + context.read().refreshCubit(); + + return const ProductsScreen(); + }, ), GoRoute( path: 'staff', // Diventa /master-data/staff @@ -172,6 +179,12 @@ class AppRouter { builder: (context, state) { final existingOperation = state.extra as OperationModel?; final operationId = state.uri.queryParameters['operationId']; + context.read().loadCustomers(); + context.read().loadActiveProvidersForStore( + GetIt.I.get().state.currentStore!.id!, + ); + context.read().loadModels(); + context.read().loadBrands(); return BlocProvider( create: (context) => OperationFilesBloc( operationId: operationId ?? existingOperation?.id, diff --git a/lib/features/customers/data/customer_repository.dart b/lib/features/customers/data/customer_repository.dart index 086db52..f7ffc8f 100644 --- a/lib/features/customers/data/customer_repository.dart +++ b/lib/features/customers/data/customer_repository.dart @@ -67,7 +67,7 @@ class CustomerRepository { .from('customer') .select() .eq('company_id', companyId) - .or('nome.ilike.%$query%,telefono.ilike.%$query%') + .or('name.ilike.%$query%,phone_number.ilike.%$query%') .limit(10); return (response as List).map((c) => CustomerModel.fromMap(c)).toList(); diff --git a/lib/features/master_data/products/blocs/product_cubit.dart b/lib/features/master_data/products/blocs/product_cubit.dart index b35a6bc..f35c0c1 100644 --- a/lib/features/master_data/products/blocs/product_cubit.dart +++ b/lib/features/master_data/products/blocs/product_cubit.dart @@ -9,19 +9,17 @@ import 'package:get_it/get_it.dart'; part 'product_state.dart'; -class ProductCubit extends Cubit { +class ProductsCubit extends Cubit { final ProductRepository _repository = GetIt.I(); final SessionCubit _sessionCubit = GetIt.I(); - ProductCubit() : super(const ProductState()); + ProductsCubit() : super(const ProductState()); // Caricamento iniziale dei Brand Future loadBrands() async { emit(state.copyWith(status: ProductStatus.loading)); try { - final brands = await _repository.getBrands( - _sessionCubit.state.company!.id!, - ); + final brands = await _repository.getBrands(); emit(state.copyWith(status: ProductStatus.success, brands: brands)); } catch (e) { emit( @@ -30,6 +28,27 @@ class ProductCubit extends Cubit { } } + Future loadModels() async { + emit(state.copyWith(status: ProductStatus.loading)); + try { + final models = await _repository.getModels(); + emit(state.copyWith(status: ProductStatus.success, models: models)); + } catch (e) { + emit( + state.copyWith(status: ProductStatus.error, errorMessage: e.toString()), + ); + } + } + + Future refreshCubit() async { + if (state.selectedBrand != null) { + await selectBrand(state.selectedBrand); + } else { + emit(state.copyWith(status: ProductStatus.initial)); + await loadBrands(); + } + } + // Selezione Brand e caricamento Modelli Future selectBrand(BrandModel? brand) async { if (brand == null) { diff --git a/lib/features/master_data/products/data/product_repository.dart b/lib/features/master_data/products/data/product_repository.dart index d456ae3..1fdbe68 100644 --- a/lib/features/master_data/products/data/product_repository.dart +++ b/lib/features/master_data/products/data/product_repository.dart @@ -1,3 +1,4 @@ +import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:get_it/get_it.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import '../models/brand_model.dart'; @@ -5,16 +6,17 @@ import '../models/model_model.dart'; class ProductRepository { final SupabaseClient _supabase = GetIt.I(); + final String _companyId = GetIt.I().state.company!.id!; // --- BRAND --- /// Recupera tutti i brand dell'azienda - Future> getBrands(String companyId) async { + Future> getBrands() async { try { final response = await _supabase .from('brand') .select() - .eq('company_id', companyId) + .eq('company_id', _companyId) .eq('is_active', true) .order('name'); @@ -57,6 +59,19 @@ class ProductRepository { } } + Future> getModels() async { + try { + final response = await _supabase + .from('model') + .select() + .eq('is_active', true) + .order('name'); + return (response as List).map((m) => ModelModel.fromJson(m)).toList(); + } catch (e) { + throw '$e'; + } + } + /// Crea o aggiorna un modello /// NOTA: name_with_brand verrà gestito dal trigger SQL che hai lanciato! Future upsertModel(ModelModel model) async { diff --git a/lib/features/master_data/products/ui/brand_selector.dart b/lib/features/master_data/products/ui/brand_selector.dart index 857bddc..2e35522 100644 --- a/lib/features/master_data/products/ui/brand_selector.dart +++ b/lib/features/master_data/products/ui/brand_selector.dart @@ -33,7 +33,7 @@ class BrandSelector extends StatelessWidget { return DropdownMenuItem(value: brand, child: Text(brand.name)); }).toList(), onChanged: (brand) => - context.read().selectBrand(brand), + context.read().selectBrand(brand), ), ), const SizedBox(width: 16), diff --git a/lib/features/master_data/products/ui/models_list.dart b/lib/features/master_data/products/ui/models_list.dart index f44e6a5..e9dd5c6 100644 --- a/lib/features/master_data/products/ui/models_list.dart +++ b/lib/features/master_data/products/ui/models_list.dart @@ -64,7 +64,7 @@ class ModelsList extends StatelessWidget { color: model.isActive ? context.accent : Colors.grey, ), onPressed: () => context - .read() + .read() .toggleStatus('model', model.id!, model.isActive), ), ], diff --git a/lib/features/master_data/products/ui/product_dialogs.dart b/lib/features/master_data/products/ui/product_dialogs.dart index 3560d0c..3fe457a 100644 --- a/lib/features/master_data/products/ui/product_dialogs.dart +++ b/lib/features/master_data/products/ui/product_dialogs.dart @@ -40,7 +40,7 @@ void _submitBrand( BrandModel? brand, ) { if (controller.text.trim().isNotEmpty) { - context.read().saveBrand(controller.text, id: brand?.id); + context.read().saveBrand(controller.text, id: brand?.id); Navigator.pop(context); } } @@ -81,7 +81,7 @@ void _submitModel( ModelModel? model, ) { if (controller.text.isNotEmpty) { - context.read().saveModel(controller.text, id: model?.id); + context.read().saveModel(controller.text, id: model?.id); Navigator.pop(context); } } diff --git a/lib/features/master_data/products/ui/products_screen.dart b/lib/features/master_data/products/ui/products_screen.dart index 28ffeab..8602876 100644 --- a/lib/features/master_data/products/ui/products_screen.dart +++ b/lib/features/master_data/products/ui/products_screen.dart @@ -12,7 +12,7 @@ class ProductsScreen extends StatelessWidget { @override Widget build(BuildContext context) { // Carichiamo i brand appena la pagina viene creata - context.read().loadBrands(); + context.read().loadBrands(); return Scaffold( backgroundColor: context.background, @@ -33,7 +33,7 @@ class ProductsScreen extends StatelessWidget { ), ), ), - body: BlocConsumer( + body: BlocConsumer( listener: (context, state) { if (state.status == ProductStatus.error) { ScaffoldMessenger.of(context).showSnackBar( diff --git a/lib/features/master_data/products/ui/quick_product_dialog.dart b/lib/features/master_data/products/ui/quick_product_dialog.dart index aa92bae..d3d1c61 100644 --- a/lib/features/master_data/products/ui/quick_product_dialog.dart +++ b/lib/features/master_data/products/ui/quick_product_dialog.dart @@ -23,7 +23,7 @@ class _QuickProductDialogState extends State { setState(() => _isLoading = true); - final newModel = await context.read().quickCreateProduct( + final newModel = await context.read().quickCreateProduct( brandName: _selectedBrandName.trim(), modelName: _modelCtrl.text.trim(), ); diff --git a/lib/features/master_data/providers/models/provider_model.dart b/lib/features/master_data/providers/models/provider_model.dart index a4a9ede..1ab1844 100644 --- a/lib/features/master_data/providers/models/provider_model.dart +++ b/lib/features/master_data/providers/models/provider_model.dart @@ -10,6 +10,7 @@ class ProviderModel extends Equatable { final bool assicurazioni; final bool intrattenimento; final bool finanziamenti; + final bool telepass; final bool altro; final bool isActive; final String companyId; @@ -24,6 +25,7 @@ class ProviderModel extends Equatable { required this.assicurazioni, required this.intrattenimento, required this.finanziamenti, + required this.telepass, required this.altro, required this.isActive, required this.companyId, @@ -51,6 +53,7 @@ class ProviderModel extends Equatable { assicurazioni: map['assicurazioni'] ?? false, intrattenimento: map['intrattenimento'] ?? false, finanziamenti: map['finanziamenti'] ?? false, + telepass: map['telepass'] ?? false, altro: map['altro'] ?? false, isActive: map['is_active'] ?? true, companyId: map['company_id'], @@ -67,6 +70,7 @@ class ProviderModel extends Equatable { 'assicurazioni': assicurazioni, 'intrattenimento': intrattenimento, 'finanziamenti': finanziamenti, + 'telepass': telepass, 'altro': altro, 'is_active': isActive, 'company_id': companyId, @@ -89,6 +93,7 @@ class ProviderModel extends Equatable { assicurazioni, intrattenimento, finanziamenti, + telepass, altro, isActive, companyId, @@ -104,6 +109,7 @@ class ProviderModel extends Equatable { bool? assicurazioni, bool? intrattenimento, bool? finanziamenti, + bool? telepass, bool? altro, bool? isActive, String? companyId, @@ -118,6 +124,7 @@ class ProviderModel extends Equatable { assicurazioni: assicurazioni ?? this.assicurazioni, intrattenimento: intrattenimento ?? this.intrattenimento, finanziamenti: finanziamenti ?? this.finanziamenti, + telepass: telepass ?? this.telepass, altro: altro ?? this.altro, isActive: isActive ?? this.isActive, companyId: companyId ?? this.companyId, diff --git a/lib/features/master_data/providers/ui/provider_form_sheet.dart b/lib/features/master_data/providers/ui/provider_form_sheet.dart index 2afc48d..37d7f6c 100644 --- a/lib/features/master_data/providers/ui/provider_form_sheet.dart +++ b/lib/features/master_data/providers/ui/provider_form_sheet.dart @@ -21,6 +21,7 @@ class _ProviderFormSheetState extends State { late bool _assicurazioni; late bool _intrattenimento; late bool _finanziamenti; + late bool _telepass; late bool _altro; late bool _isActive; final List _tempSelectedStoreIds = @@ -40,6 +41,7 @@ class _ProviderFormSheetState extends State { _assicurazioni = p?.assicurazioni ?? false; _intrattenimento = p?.intrattenimento ?? false; _finanziamenti = p?.finanziamenti ?? false; + _telepass = p?.telepass ?? false; _altro = p?.altro ?? false; _isActive = p?.isActive ?? true; } @@ -64,6 +66,7 @@ class _ProviderFormSheetState extends State { assicurazioni: _assicurazioni, intrattenimento: _intrattenimento, finanziamenti: _finanziamenti, + telepass: _telepass, altro: _altro, isActive: _isActive, companyId: @@ -138,6 +141,11 @@ class _ProviderFormSheetState extends State { _finanziamenti, (v) => setState(() => _finanziamenti = v), ), + _buildSwitch( + "Telepass", + _telepass, + (v) => setState(() => _telepass = v), + ), _buildSwitch( "Altro/Accessori", _altro, 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 index bc69fe0..8d4a259 100644 --- a/lib/features/master_data/providers/ui/providers_master_data_screen.dart +++ b/lib/features/master_data/providers/ui/providers_master_data_screen.dart @@ -146,6 +146,8 @@ class _ProvidersMasterDataScreenState extends State { if (p.energia) _smallTag("⚡ Energy", Colors.orange), if (p.assicurazioni) _smallTag("🛡️ Assic", Colors.teal), if (p.intrattenimento) _smallTag("📺 Ent", Colors.red), + if (p.finanziamenti) _smallTag("💰 Fin", Colors.purple), + if (p.telepass) _smallTag("🏎️ Telepass", Colors.yellow), if (p.altro) _smallTag("📦 Altro", Colors.grey), ], ); diff --git a/lib/features/operations/blocs/operations_cubit.dart b/lib/features/operations/blocs/operations_cubit.dart index 88f4441..1b4b0e9 100644 --- a/lib/features/operations/blocs/operations_cubit.dart +++ b/lib/features/operations/blocs/operations_cubit.dart @@ -213,14 +213,19 @@ class OperationsCubit extends Cubit { String? customerDisplayName, String? type, String? providerId, + String? providerDisplayName, String? subtype, DateTime? expirationDate, int? quantity, + String? modelId, + String? modelDisplayName, // Aggiungiamo questi flag per forzare la pulizia dei campi quando cambi tipo bool clearProvider = false, bool clearType = false, bool clearSubtype = false, bool clearExpiration = false, + bool clearQuantity = false, + bool clearModel = false, }) { if (state.currentOperation == null) return; @@ -231,16 +236,48 @@ class OperationsCubit extends Cubit { final updated = current.copyWith( customerId: customerId, customerDisplayName: customerDisplayName, - type: clearType ? null : type, - subtype: clearSubtype ? null : subtype, - expirationDate: clearExpiration ? null : expirationDate, + // Se clearProvider è true, forziamo una stringa vuota (o null se il tuo modello lo supporta) providerId: clearProvider ? null : (providerId ?? current.providerId), - // Idem per subtype e date. - // Se expirationDate è nullabile nel copyWith, dovresti poterlo gestire - quantity: quantity ?? current.quantity, + providerDisplayName: clearProvider + ? null + : (providerDisplayName ?? current.providerDisplayName), + quantity: clearQuantity ? 1 : (quantity ?? current.quantity), + type: clearType ? null : (type ?? current.type), + subtype: clearSubtype ? null : (subtype ?? current.subtype), + expirationDate: clearExpiration + ? null + : (expirationDate ?? current.expirationDate), + modelId: clearModel ? null : (modelId ?? current.modelId), + modelDisplayName: clearModel + ? null + : (modelDisplayName ?? current.modelDisplayName), ); emit(state.copyWith(currentOperation: updated)); } + + // Metodo di utilità per calcolare la data X mesi da oggi + DateTime _calculateMonths(int months) { + final now = DateTime.now(); + return DateTime(now.year, now.month + months, now.day); + } + + // Quando l'utente seleziona un tipo, impostiamo il default + void setTypeWithSmartDefault(String type) { + DateTime? defaultDate; + + if (type == 'Energy') defaultDate = _calculateMonths(24); + if (type == 'Fin') defaultDate = _calculateMonths(30); + if (type == 'Entertainment') defaultDate = _calculateMonths(12); + + updateOperationFields( + type: type, + expirationDate: defaultDate, + clearProvider: true, + clearSubtype: true, + clearModel: true, + clearQuantity: true, + ); + } } diff --git a/lib/features/operations/ui/operation_form_screen.dart b/lib/features/operations/ui/operation_form_screen.dart index ce185d0..dd48807 100644 --- a/lib/features/operations/ui/operation_form_screen.dart +++ b/lib/features/operations/ui/operation_form_screen.dart @@ -1,6 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/features/customers/blocs/customers_cubit.dart'; +import 'package:flux/features/customers/ui/quick_customer_dialog.dart'; +import 'package:flux/features/master_data/products/blocs/product_cubit.dart'; +import 'package:flux/features/master_data/products/ui/quick_product_dialog.dart'; +import 'package:flux/features/master_data/providers/blocs/provider_cubit.dart'; import 'package:flux/features/operations/blocs/operations_cubit.dart'; import 'package:flux/features/operations/models/operation_model.dart'; // import 'package:flux/features/attachments/ui/operation_files_section.dart'; @@ -25,7 +29,7 @@ class _OperationFormScreenState extends State { // TEXT CONTROLLERS (Unici detentori di stato locale per evitare lag) final _referenceController = TextEditingController(); final _noteController = TextEditingController(); - final _customSubtypeController = TextEditingController(); + final _freeTextSubtypeController = TextEditingController(); final List _availableTypes = [ 'AL', @@ -39,6 +43,34 @@ class _OperationFormScreenState extends State { 'Custom', ]; + bool _doesProviderMatchOperationType(dynamic provider, String operationType) { + // Se è Custom o non riconosciuto, mostriamo tutto + if (operationType == 'Custom') return true; + + // Qui mappiamo il tipo di operazione scelto con i bool del ProviderModel + switch (operationType) { + case 'AL': + return provider.telefoniaMobile == true; + case 'MNP': + return provider.telefoniaMobile == true; + case 'NIP': + return provider.telefoniaFissa == true; + case 'UNICA': + return provider.telefoniaFissa == true || + provider.telefoniaMobile == true; + case 'Energy': + return provider.energia == true; + case 'Fin': + return provider.finanziamenti == true; + case 'Entertainment': + return provider.intrattenimento == true; + case 'TELEPASS': + return provider.telepass == true; + default: + return true; + } + } + bool _isInitialized = false; @override @@ -55,7 +87,7 @@ class _OperationFormScreenState extends State { void dispose() { _referenceController.dispose(); _noteController.dispose(); - _customSubtypeController.dispose(); + _freeTextSubtypeController.dispose(); super.dispose(); } @@ -67,6 +99,11 @@ class _OperationFormScreenState extends State { if (_noteController.text.isEmpty && model.note.isNotEmpty) { _noteController.text = model.note; } + if (_freeTextSubtypeController.text.isEmpty && + model.subtype != null && + model.subtype!.isNotEmpty) { + _freeTextSubtypeController.text = model.subtype!; + } _isInitialized = true; } @@ -96,6 +133,8 @@ class _OperationFormScreenState extends State { // --- MODALE SELEZIONE CLIENTE --- void _showCustomerModal() { + String currentSearchQuery = ''; + showModalBottomSheet( context: context, isScrollControlled: true, @@ -132,6 +171,7 @@ class _OperationFormScreenState extends State { Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: TextField( + autofocus: true, decoration: InputDecoration( hintText: 'Cerca per nome, telefono o email...', prefixIcon: const Icon(Icons.search), @@ -140,8 +180,8 @@ class _OperationFormScreenState extends State { ), ), onChanged: (query) { - // Evento di ricerca (usa debouncer nel cubit!) - // context.read().searchCustomers(query); + currentSearchQuery = query; + context.read().searchCustomers(query); }, ), ), @@ -154,8 +194,37 @@ class _OperationFormScreenState extends State { ), icon: const Icon(Icons.person_add), label: const Text('Crea Nuovo Cliente'), - onPressed: () { - // Apri form nuovo cliente... + onPressed: () async { + final OperationsCubit operationsCubit = context + .read(); + + // APRIAMO LA DIALOG RAPIDA CON LA MAGIA DEL BLOC PROVIDER + final newCustomer = await showDialog( + context: context, + builder: (dialogContext) { + return BlocProvider.value( + value: context.read(), + child: QuickCustomerDialog( + initialQuery: + currentSearchQuery, // <-- Passiamo quello che ha digitato! + ), + ); + }, + ); + + // Se l'ha creato davvero (e non ha premuto annulla)... + if (newCustomer != null) { + // 1. Aggiorniamo il form delle operazioni + operationsCubit.updateOperationFields( + customerId: newCustomer.id, + customerDisplayName: newCustomer.name, + ); + + // 2. Chiudiamo la BottomSheet dei clienti per tornare alla form! + if (context.mounted) { + Navigator.pop(modalContext); + } + } }, ), ), @@ -249,7 +318,7 @@ class _OperationFormScreenState extends State { // Ripuliamo SOLO i testi liberi (il Cubit gestisce già i suoi reset) _referenceController.clear(); _noteController.clear(); - _customSubtypeController.clear(); + _freeTextSubtypeController.clear(); } else if (state.status == OperationsStatus.failure) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -403,12 +472,7 @@ class _OperationFormScreenState extends State { onSelected: (selected) { if (selected) { // Diciamo al Cubit di cambiare tipo e spianare i campi dipendenti - context.read().updateOperationFields( - type: type, - clearProvider: true, - clearSubtype: true, - clearExpiration: true, - ); + context.read().setTypeWithSmartDefault(type); } }, ); @@ -423,73 +487,138 @@ class _OperationFormScreenState extends State { ListTile( title: const Text('Seleziona Gestore'), subtitle: Text( - currentOp?.providerId ?? 'Nessun gestore selezionato', - ), // Adatta se hai displayName + (currentOp?.providerDisplayName != null && + currentOp!.providerDisplayName!.isNotEmpty) + ? currentOp.providerDisplayName! + : 'Nessun gestore selezionato', + style: TextStyle( + color: + (currentOp?.providerId == null || + currentOp!.providerId!.isEmpty) + ? Colors.grey + : null, + fontWeight: + (currentOp?.providerId == null || + currentOp!.providerId!.isEmpty) + ? FontWeight.normal + : FontWeight.bold, + ), + ), trailing: const Icon(Icons.arrow_drop_down), shape: RoundedRectangleBorder( side: BorderSide(color: theme.dividerColor), borderRadius: BorderRadius.circular(8), ), onTap: () { - // TODO: Modale o Dropdown Provider + _showProviderModal(currentType); }, ), const SizedBox(height: 16), - // SOTTO-TIPO (Reattivo) - if (['Energy', 'Fin', 'Entertainment'].contains(currentType)) ...[ - DropdownButtonFormField( + // 1. SCENARIO ENERGY (Dropdown Fisso) + if (currentType == 'Energy') ...[ + DropdownButtonFormField( initialValue: - null, // Sostituisci con currentOp?.subtype quando lo aggiungi - decoration: const InputDecoration( - labelText: 'Dettaglio (es. Luce, Gas...)', - ), + (currentOp?.subtype != null && currentOp!.subtype!.isNotEmpty) + ? currentOp.subtype + : null, + decoration: const InputDecoration(labelText: 'Dettaglio Fornitura'), items: [ 'Luce', 'Gas', 'Dual', ].map((s) => DropdownMenuItem(value: s, child: Text(s))).toList(), onChanged: (val) { - // context.read().updateOperationFields(subtype: val); + if (val != null) { + context.read().updateOperationFields( + subtype: val, + ); + } }, ), const SizedBox(height: 16), ], - // SOTTO-TIPO CUSTOM (Reattivo) - if (currentType == 'Custom') ...[ + // 2. SCENARIO FIN (Ricerca Modello/Prodotto) + if (currentType == 'Fin') ...[ + ListTile( + title: const Text('Seleziona Dispositivo/Prodotto'), + subtitle: Text( + (currentOp?.modelDisplayName != null && + currentOp!.modelDisplayName!.isNotEmpty) + ? currentOp.modelDisplayName! + : 'Nessun modello selezionato', + style: TextStyle( + color: + (currentOp?.modelId == null || currentOp!.modelId!.isEmpty) + ? Colors.grey + : null, + fontWeight: + (currentOp?.modelId == null || currentOp!.modelId!.isEmpty) + ? FontWeight.normal + : FontWeight.bold, + ), + ), + trailing: const Icon(Icons.arrow_drop_down), + shape: RoundedRectangleBorder( + side: BorderSide(color: theme.dividerColor), + borderRadius: BorderRadius.circular(8), + ), + onTap: _showModelModal, + ), + const SizedBox(height: 16), + ], + + // 3. SCENARIO ENTERTAINMENT O CUSTOM (Testo libero) + if (currentType == 'Entertainment' || currentType == 'Custom') ...[ TextFormField( - controller: _customSubtypeController, - decoration: const InputDecoration( - labelText: 'Specifica il servizio (es. Monopattino)', + controller: _freeTextSubtypeController, + decoration: InputDecoration( + labelText: currentType == 'Entertainment' + ? 'Piattaforma (es. Netflix, DAZN, Spotify...)' + : 'Specifica il servizio (es. Monopattino)', ), ), const SizedBox(height: 16), ], - // SCADENZA (Reattivo) + // SCADENZA (Reattivo per tipi complessi) if ([ 'Energy', 'Fin', 'Entertainment', 'Custom', ].contains(currentType)) ...[ + const SizedBox(height: 8), + + // --- I CHIPS RAPIDI --- + _buildDurationQuickPicks(currentOp), + + const SizedBox(height: 16), + + // --- IL SELETTORE MANUALE --- ListTile( - title: const Text('Data di Scadenza'), + title: const Text('Data di Scadenza Effettiva'), subtitle: Text( - currentOp?.expirationDate?.toLocal().toString().split(' ')[0] ?? - 'Nessuna scadenza', + currentOp?.expirationDate != null + ? "${currentOp!.expirationDate!.day}/${currentOp.expirationDate!.month}/${currentOp.expirationDate!.year}" + : 'Nessuna scadenza impostata', + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), ), - trailing: const Icon(Icons.calendar_today), + trailing: const Icon(Icons.calendar_month, color: Colors.blue), + tileColor: Colors.blue.withValues(alpha: 0.05), shape: RoundedRectangleBorder( - side: BorderSide(color: theme.dividerColor), - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(12), + side: const BorderSide(color: Colors.blue, width: 0.5), ), onTap: () async { - final operationsCubit = context.read(); + final OperationsCubit operationsCubit = context + .read(); final date = await showDatePicker( context: context, - initialDate: DateTime.now().add(const Duration(days: 365)), + initialDate: + currentOp?.expirationDate ?? + DateTime.now().add(const Duration(days: 365)), firstDate: DateTime.now(), lastDate: DateTime.now().add(const Duration(days: 3650)), ); @@ -545,6 +674,50 @@ class _OperationFormScreenState extends State { ); } + Widget _buildDurationQuickPicks(OperationModel? currentOp) { + final durations = [3, 6, 12, 24, 30, 36, 48]; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Imposta durata rapida (mesi):", + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Colors.grey, + ), + ), + const SizedBox(height: 8), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: durations.map((months) { + return Padding( + padding: const EdgeInsets.only(right: 8.0), + child: ActionChip( + label: Text("$months m"), + backgroundColor: Colors.blue.withValues(alpha: 0.05), + onPressed: () { + final now = DateTime.now(); + final newDate = DateTime( + now.year, + now.month + months, + now.day, + ); + context.read().updateOperationFields( + expirationDate: newDate, + ); + }, + ), + ); + }).toList(), + ), + ), + ], + ); + } + Widget _buildNotesSection({required bool isDesktop}) { final title = _buildSectionTitle('Note Interne'); final noteField = TextFormField( @@ -626,4 +799,247 @@ class _OperationFormScreenState extends State { ), ); } + + void _showProviderModal(String currentOperationType) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + builder: (modalContext) { + return DraggableScrollableSheet( + initialChildSize: 0.5, // Parte a metà schermo + minChildSize: 0.4, + maxChildSize: 0.8, + expand: false, + builder: (_, scrollController) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Seleziona Gestore', + style: Theme.of(context).textTheme.titleLarge, + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.pop(modalContext), + ), + ], + ), + ), + const Divider(), + Expanded( + child: BlocBuilder( + // <--- Usa il tuo Cubit dei provider + builder: (context, state) { + if (state.isLoading) { + return const Center(child: CircularProgressIndicator()); + } + + // Simuliamo la lista di provider caricata dal tuo stato + final allProviders = state.activeProviders; + + // Applichiamo il nostro filtro magico! + final filteredProviders = allProviders + .where( + (p) => _doesProviderMatchOperationType( + p, + currentOperationType, + ), + ) + .toList(); + + if (filteredProviders.isEmpty) { + return const Center( + child: Text( + 'Nessun gestore compatibile con questo servizio.', + style: TextStyle(color: Colors.grey), + ), + ); + } + + return ListView.builder( + controller: scrollController, + itemCount: filteredProviders.length, + itemBuilder: (context, index) { + final provider = filteredProviders[index]; + + return ListTile( + leading: const Icon(Icons.business), + title: Text( + provider.nome, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + onTap: () { + // Selezione effettuata! Diciamo al Cubit delle operazioni di aggiornarsi + context + .read() + .updateOperationFields( + providerId: provider.id, + providerDisplayName: provider + .nome, // Fondamentale per la UI! + ); + Navigator.pop(modalContext); + }, + ); + }, + ); + }, + ), + ), + ], + ); + }, + ); + }, + ); + } + + // --- MODALE SELEZIONE MODELLO (PER FINANZIAMENTI) --- + void _showModelModal() { + showModalBottomSheet( + context: context, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + builder: (modalContext) { + return DraggableScrollableSheet( + initialChildSize: 0.6, + minChildSize: 0.4, + maxChildSize: 0.9, + expand: false, + builder: (_, scrollController) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Seleziona Modello', + style: Theme.of(context).textTheme.titleLarge, + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.pop(modalContext), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: TextField( + decoration: InputDecoration( + hintText: 'Cerca modello (es. iPhone 15...)', + prefixIcon: const Icon(Icons.search), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onChanged: (query) { + context.read().searchModels(query); + }, + ), + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton.icon( + style: ElevatedButton.styleFrom( + minimumSize: const Size.fromHeight(48), + ), + icon: const Icon(Icons.add), + label: const Text('Aggiungi Modello al Volo'), + onPressed: () async { + final OperationsCubit operationsCubit = context + .read(); + + // 1. Recuperiamo la lista dei brand (adatta questo in base a dove tieni i brand nel tuo stato) + final existingBrands = context + .read() + .state + .brands; // <-- Verifica che sia corretto! + + // 2. Apriamo il tuo Dialog. + // ATTENZIONE DA CECCHINO: showDialog crea una nuova "rotta" sopra l'albero dei widget, + // quindi dobbiamo passargli il Cubit usando BlocProvider.value per non farglielo perdere! + final newModel = await showDialog( + context: context, + builder: (dialogContext) { + return BlocProvider.value( + value: context.read(), + child: QuickProductDialog( + existingBrands: existingBrands, + ), + ); + }, + ); + + // 3. Se l'utente ha effettivamente creato un modello e non ha premuto "Annulla"... + if (newModel != null) { + // A. Aggiorniamo il form del Cubit delle operazioni con il nuovo nato! + operationsCubit.updateOperationFields( + modelId: newModel.id, + modelDisplayName: newModel + .nameWithBrand, // <-- Verifica il nome della property + ); + + // B. Chiudiamo ANCHE la BottomSheet dei modelli per far tornare l'utente al form principale + if (context.mounted) { + Navigator.pop(modalContext); + } + } + }, + ), + ), + const Divider(), + Expanded( + child: BlocBuilder( + // <--- Usa il tuo Cubit dei modelli! + builder: (context, state) { + return ListView.builder( + controller: scrollController, + itemCount: state + .models + .length, // Sostituisci con state.models.length + itemBuilder: (context, index) { + final deviceModel = state.models[index]; + return ListTile( + leading: const Icon(Icons.devices), + title: Text( + deviceModel.nameWithBrand, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + onTap: () { + context.read().updateOperationFields( + modelId: + 'id_del_modello_$index', // deviceModel.id + // Assicurati di avere questo campo in _updateOperationFields nel Cubit! + // modelDisplayName: deviceModel.nameWithBrand, + ); + Navigator.pop(modalContext); + }, + ); + }, + ); + }, + ), + ), + ], + ); + }, + ); + }, + ); + } } diff --git a/lib/main.dart b/lib/main.dart index 9d86a5b..93198f7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -49,7 +49,7 @@ void main() async { // Cubit delle feature BlocProvider(create: (_) => StoreCubit()), BlocProvider(create: (_) => CustomersCubit()), - BlocProvider(create: (_) => ProductCubit()), + BlocProvider(create: (_) => ProductsCubit()), BlocProvider(create: (_) => StaffCubit()), BlocProvider(create: (_) => OperationsCubit()), BlocProvider(create: (_) => ProvidersCubit()),