diff --git a/lib/core/data/core_repository.dart b/lib/core/data/core_repository.dart index 6aeac53..2998a5c 100644 --- a/lib/core/data/core_repository.dart +++ b/lib/core/data/core_repository.dart @@ -50,7 +50,7 @@ class CoreRepository { .select() .eq('company_id', companyId) .eq('is_active', true) // Buona pratica - .order('nome'); // O come si chiama il campo nome + .order('name'); // O come si chiama il campo nome return (response as List).map((s) => StoreModel.fromMap(s)).toList(); } catch (e) { diff --git a/lib/features/master_data/providers/data/provider_repository.dart b/lib/features/master_data/providers/data/provider_repository.dart index ef5e271..54b377d 100644 --- a/lib/features/master_data/providers/data/provider_repository.dart +++ b/lib/features/master_data/providers/data/provider_repository.dart @@ -51,7 +51,7 @@ class ProviderRepository { ) ''') .eq('company_id', companyId) - .order('nome'); + .order('name'); return (response as List).map((m) => ProviderModel.fromMap(m)).toList(); } catch (e) { diff --git a/lib/features/master_data/providers/models/provider_model.dart b/lib/features/master_data/providers/models/provider_model.dart index 1ab1844..5027f30 100644 --- a/lib/features/master_data/providers/models/provider_model.dart +++ b/lib/features/master_data/providers/models/provider_model.dart @@ -3,30 +3,30 @@ import 'package:flux/features/master_data/store/models/store_model.dart'; class ProviderModel extends Equatable { final String? id; - final String nome; - final bool telefoniaFissa; - final bool telefoniaMobile; - final bool energia; - final bool assicurazioni; - final bool intrattenimento; - final bool finanziamenti; + 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 altro; + final bool other; final bool isActive; final String companyId; final List associatedStores; const ProviderModel({ this.id, - required this.nome, - required this.telefoniaFissa, - required this.telefoniaMobile, - required this.energia, - required this.assicurazioni, - required this.intrattenimento, - required this.finanziamenti, + 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.altro, + required this.other, required this.isActive, required this.companyId, this.associatedStores = const [], @@ -46,15 +46,15 @@ class ProviderModel extends Equatable { } return ProviderModel( id: map['id'], - nome: map['nome'], - telefoniaFissa: map['telefonia_fissa'] ?? false, - telefoniaMobile: map['telefonia_mobile'] ?? false, - energia: map['energia'] ?? false, - assicurazioni: map['assicurazioni'] ?? false, - intrattenimento: map['intrattenimento'] ?? false, - finanziamenti: map['finanziamenti'] ?? false, + 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, - altro: map['altro'] ?? false, + other: map['other'] ?? false, isActive: map['is_active'] ?? true, companyId: map['company_id'], associatedStores: stores, @@ -63,15 +63,15 @@ class ProviderModel extends Equatable { Map toMap() { final map = { - 'nome': nome, - 'telefonia_fissa': telefoniaFissa, - 'telefonia_mobile': telefoniaMobile, - 'energia': energia, - 'assicurazioni': assicurazioni, - 'intrattenimento': intrattenimento, - 'finanziamenti': finanziamenti, + 'name': name, + 'landline': landline, + 'mobile': mobile, + 'energy': energy, + 'insurance': insurance, + 'entertainment': entertainment, + 'financing': financing, 'telepass': telepass, - 'altro': altro, + 'other': other, 'is_active': isActive, 'company_id': companyId, }; @@ -86,15 +86,15 @@ class ProviderModel extends Equatable { @override List get props => [ id, - nome, - telefoniaFissa, - telefoniaMobile, - energia, - assicurazioni, - intrattenimento, - finanziamenti, + name, + landline, + mobile, + energy, + insurance, + entertainment, + financing, telepass, - altro, + other, isActive, companyId, associatedStores, @@ -102,30 +102,30 @@ class ProviderModel extends Equatable { ProviderModel copyWith({ String? id, - String? nome, - bool? telefoniaFissa, - bool? telefoniaMobile, - bool? energia, - bool? assicurazioni, - bool? intrattenimento, - bool? finanziamenti, + String? name, + bool? landline, + bool? mobile, + bool? energy, + bool? insurance, + bool? entertainment, + bool? financing, bool? telepass, - bool? altro, + bool? other, bool? isActive, String? companyId, List? associatedStores, }) { return ProviderModel( id: id ?? this.id, - nome: nome ?? this.nome, - telefoniaFissa: telefoniaFissa ?? this.telefoniaFissa, - telefoniaMobile: telefoniaMobile ?? this.telefoniaMobile, - energia: energia ?? this.energia, - assicurazioni: assicurazioni ?? this.assicurazioni, - intrattenimento: intrattenimento ?? this.intrattenimento, - finanziamenti: finanziamenti ?? this.finanziamenti, + 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, - altro: altro ?? this.altro, + other: other ?? this.other, isActive: isActive ?? this.isActive, companyId: companyId ?? this.companyId, associatedStores: associatedStores ?? this.associatedStores, 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 fbf1602..0fead8a 100644 --- a/lib/features/master_data/providers/ui/provider_form_sheet.dart +++ b/lib/features/master_data/providers/ui/provider_form_sheet.dart @@ -15,14 +15,14 @@ class ProviderFormSheet extends StatefulWidget { class _ProviderFormSheetState extends State { late TextEditingController _nameController; - late bool _telefoniaFissa; - late bool _telefoniaMobile; - late bool _energia; - late bool _assicurazioni; - late bool _intrattenimento; - late bool _finanziamenti; + late bool _landline; + late bool _mobile; + late bool _energy; + late bool _insurance; + late bool _entertainment; + late bool _financing; late bool _telepass; - late bool _altro; + late bool _other; late bool _isActive; final List _tempSelectedStoreIds = []; // Per gestire la selezione temporanea dei negozi @@ -34,15 +34,15 @@ class _ProviderFormSheetState extends State { for (final store in p?.associatedStores ?? []) { _tempSelectedStoreIds.add(store.id!); } - _nameController = TextEditingController(text: p?.nome ?? ''); - _telefoniaFissa = p?.telefoniaFissa ?? false; - _telefoniaMobile = p?.telefoniaMobile ?? false; - _energia = p?.energia ?? false; - _assicurazioni = p?.assicurazioni ?? false; - _intrattenimento = p?.intrattenimento ?? false; - _finanziamenti = p?.finanziamenti ?? false; + _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; - _altro = p?.altro ?? false; + _other = p?.other ?? false; _isActive = p?.isActive ?? true; } @@ -59,15 +59,15 @@ class _ProviderFormSheetState extends State { final cubit = context.read(); final provider = ProviderModel( id: widget.initialProvider?.id, // Se nullo, Supabase farà insert - nome: _nameController.text.trim(), - telefoniaFissa: _telefoniaFissa, - telefoniaMobile: _telefoniaMobile, - energia: _energia, - assicurazioni: _assicurazioni, - intrattenimento: _intrattenimento, - finanziamenti: _finanziamenti, + name: _nameController.text.trim(), + landline: _landline, + mobile: _mobile, + energy: _energy, + insurance: _insurance, + entertainment: _entertainment, + financing: _financing, telepass: _telepass, - altro: _altro, + other: _other, isActive: _isActive, companyId: '', // Verrà ignorato dal toMap e gestito dal Cubit/SessionBloc se hai messo la logica lì @@ -113,33 +113,33 @@ class _ProviderFormSheetState extends State { ), _buildSwitch( "Energia (Luce/Gas)", - _energia, - (v) => setState(() => _energia = v), + _energy, + (v) => setState(() => _energy = v), ), _buildSwitch( "Telefonia Fissa", - _telefoniaFissa, - (v) => setState(() => _telefoniaFissa = v), + _landline, + (v) => setState(() => _landline = v), ), _buildSwitch( "Telefonia Mobile", - _telefoniaMobile, - (v) => setState(() => _telefoniaMobile = v), + _mobile, + (v) => setState(() => _mobile = v), ), _buildSwitch( "Assicurazioni", - _assicurazioni, - (v) => setState(() => _assicurazioni = v), + _insurance, + (v) => setState(() => _insurance = v), ), _buildSwitch( "Intrattenimento", - _intrattenimento, - (v) => setState(() => _intrattenimento = v), + _entertainment, + (v) => setState(() => _entertainment = v), ), _buildSwitch( "Finanziamenti", - _finanziamenti, - (v) => setState(() => _finanziamenti = v), + _financing, + (v) => setState(() => _financing = v), ), _buildSwitch( "Telepass", @@ -148,8 +148,8 @@ class _ProviderFormSheetState extends State { ), _buildSwitch( "Altro/Accessori", - _altro, - (v) => setState(() => _altro = v), + _other, + (v) => setState(() => _other = v), ), const Divider(), _buildSwitch( 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 8d4a259..465f4da 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 @@ -93,7 +93,7 @@ class _ProvidersMasterDataScreenState extends State { ), ), title: Text( - provider.nome, + provider.name, style: const TextStyle(fontWeight: FontWeight.bold), ), subtitle: _buildCardSubtitle( @@ -141,14 +141,13 @@ class _ProvidersMasterDataScreenState extends State { return Wrap( spacing: 4, children: [ - if (p.telefoniaFissa || p.telefoniaMobile) - _smallTag("📞 Tel", Colors.blue), - 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.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.altro) _smallTag("📦 Altro", Colors.grey), + if (p.other) _smallTag("📦 Altro", Colors.grey), ], ); } diff --git a/lib/features/master_data/store/data/store_repository.dart b/lib/features/master_data/store/data/store_repository.dart index 01e52c1..0d64d91 100644 --- a/lib/features/master_data/store/data/store_repository.dart +++ b/lib/features/master_data/store/data/store_repository.dart @@ -98,7 +98,7 @@ class StoreRepository { ) ''') .eq('company_id', companyId) - .order('nome'); + .order('name'); return (response as List).map((m) => StoreModel.fromMap(m)).toList(); } catch (e) { diff --git a/lib/features/master_data/store/ui/store_card.dart b/lib/features/master_data/store/ui/store_card.dart index a585254..1c2d8b4 100644 --- a/lib/features/master_data/store/ui/store_card.dart +++ b/lib/features/master_data/store/ui/store_card.dart @@ -191,7 +191,7 @@ class _StoreCardState extends State { (p) => p.id == provider.id, ); return CheckboxListTile( - title: Text(provider.nome), + title: Text(provider.name), value: isAssociated, onChanged: (selected) { if (selected == true) { diff --git a/lib/features/operations/blocs/operations_cubit.dart b/lib/features/operations/blocs/operations_cubit.dart index 1b4b0e9..be48feb 100644 --- a/lib/features/operations/blocs/operations_cubit.dart +++ b/lib/features/operations/blocs/operations_cubit.dart @@ -165,7 +165,7 @@ class OperationsCubit extends Cubit { ); final updatedOperation = await _repository.saveFullOperation( - operationToSave, + operation: operationToSave, ); emit( @@ -233,6 +233,16 @@ class OperationsCubit extends Cubit { // Creiamo il modello aggiornato // ATTENZIONE: adatta questa logica in base a come è scritto il tuo copyWith! + int? newQuantity; + if (clearQuantity) { + newQuantity = 1; + } + if (quantity != null && quantity <= 0) { + newQuantity = 0; + } + if (quantity != null && quantity > 0) { + newQuantity = quantity; + } final updated = current.copyWith( customerId: customerId, customerDisplayName: customerDisplayName, @@ -242,7 +252,7 @@ class OperationsCubit extends Cubit { providerDisplayName: clearProvider ? null : (providerDisplayName ?? current.providerDisplayName), - quantity: clearQuantity ? 1 : (quantity ?? current.quantity), + quantity: newQuantity, type: clearType ? null : (type ?? current.type), subtype: clearSubtype ? null : (subtype ?? current.subtype), expirationDate: clearExpiration diff --git a/lib/features/operations/data/operations_repository.dart b/lib/features/operations/data/operations_repository.dart index bafa985..2d38fb8 100644 --- a/lib/features/operations/data/operations_repository.dart +++ b/lib/features/operations/data/operations_repository.dart @@ -99,38 +99,35 @@ class OperationsRepository { } // --- SALVATAGGIO COMPLETO (PRIMA PADRE, POI FIGLI) --- - Future saveFullOperation(OperationModel operation) async { + Future saveFullOperation({ + required OperationModel operation, + }) async { try { - // 1. Upsert del record principale - final operationData = await _supabase + // 1. Salvataggio classico dell'operazione corrente + final response = await _supabase .from('operation') .upsert(operation.toMap()) - .select() + .select( + '*, provider(*), model(*), store(*), staff_member(*), customer(*), attachment(*)', + ) .single(); - final String newId = operationData['id']; + final savedOperation = OperationModel.fromMap(response); - // 5. GRAN FINALE: RECUPERO DEL MODELLO COMPLETO E AGGIORNATO - // Interroghiamo Supabase per farci restituire la pratica con TUTTI gli ID generati - // (inclusi quelli della tabella operation_file appena inseriti) - final updatedOperationData = await _supabase - .from('operation') - .select(''' - *, - staff_member(name), - store(name), - provider(name), - model(name_with_brand), - customer(name), - attachment(*) - ''') - .eq('id', newId) - .single(); + // 2. ALLINEAMENTO BATCH SEMPRE ATTIVO! + if (operation.batchUuid.isNotEmpty) { + await _supabase + .from('operation') + .update({'note': operation.note}) // Spalmiamo la nota attuale + .eq( + 'batch_uuid', + operation.batchUuid, + ); // Su tutte le pratiche di questo scontrino + } - return OperationModel.fromMap(updatedOperationData); + return savedOperation; } catch (e) { - // Qui potresti aggiungere una logica di "rollback manuale" se necessario - throw Exception('$e'); + throw Exception("Errore nel salvataggio dell'operazione: $e"); } } diff --git a/lib/features/operations/models/operation_model.dart b/lib/features/operations/models/operation_model.dart index ef2c0b6..57efa90 100644 --- a/lib/features/operations/models/operation_model.dart +++ b/lib/features/operations/models/operation_model.dart @@ -169,35 +169,49 @@ class OperationModel extends Equatable { factory OperationModel.fromMap(Map map) { return OperationModel( - id: map['id'], + id: map['id'] as String?, createdAt: map['created_at'] != null ? DateTime.parse(map['created_at']) : null, type: map['type'] as String? ?? '', subtype: map['sub_type'] as String?, - providerId: map['provider_id'] as String? ?? '', - providerDisplayName: "${map['provider']['name']}".myFormat(), - modelId: map['model_id'] as String? ?? '', - modelDisplayName: "${map['model']['name_with_brand'] ?? ''}".myFormat(), - description: map['description'] as String? ?? '', + + // I campi relazionali nullabili restano rigorosamente null! + providerId: map['provider_id'] as String?, + // MAGIA ANTI-CRASH: Usiamo ?['chiave'] per non far esplodere i join vuoti + providerDisplayName: (map['provider']?['name'] as String?)?.myFormat(), + + modelId: map['model_id'] as String?, + modelDisplayName: (map['model']?['name_with_brand'] as String?) + ?.myFormat(), + + description: map['description'] as String?, expirationDate: map['expiration_date'] != null ? DateTime.parse(map['expiration_date']) : null, note: map['note'] as String? ?? '', - showInDashboard: map['show_in_dashboard'] as bool, - batchUuid: map['batch_uuid'] as String, + showInDashboard: map['show_in_dashboard'] as bool? ?? true, + batchUuid: map['batch_uuid'] as String? ?? '', companyId: map['company_id'] as String, - storeId: map['store_id'] as String? ?? '', - storeDisplayName: "${map['store']['name']}".myFormat(), + + storeId: + map['store_id'] as String? ?? + '', // Questo è non-nullable nella tua classe + storeDisplayName: (map['store']?['name'] as String?)?.myFormat(), + quantity: map['quantity'] is int ? map['quantity'] - : int.tryParse(map['quantity']?.toString() ?? '0') ?? 0, - staffId: map['staff_id'] as String? ?? '', - staffDisplayName: "${map['staff_member']['name'] ?? ''}".myFormat(), - lastCampaignId: map['last_campaign_id'] as String? ?? '', - status: OperationStatus.fromString(map['status']), - customerId: map['customer_id'] as String? ?? '', - customerDisplayName: "${map['customer']['name'] ?? ''}".myFormat(), + : int.tryParse(map['quantity']?.toString() ?? '1') ?? 1, + + staffId: map['staff_id'] as String?, + staffDisplayName: (map['staff_member']?['name'] as String?)?.myFormat(), + + lastCampaignId: map['last_campaign_id'] as String?, + status: OperationStatus.fromString(map['status'] ?? 'draft'), + + customerId: map['customer_id'] as String?, + customerDisplayName: (map['customer']?['name'] as String?)?.myFormat(), + attachments: (map['attachment'] as List?) ?.map((x) => AttachmentModel.fromMap(x)) diff --git a/lib/features/operations/ui/operation_form_screen.dart b/lib/features/operations/ui/operation_form_screen.dart index bbdf1e7..ca9b24b 100644 --- a/lib/features/operations/ui/operation_form_screen.dart +++ b/lib/features/operations/ui/operation_form_screen.dart @@ -1,12 +1,9 @@ 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/operations/ui/widgets/customer_section.dart'; +import 'package:flux/features/operations/ui/widgets/details_section.dart'; // ASSICURATI DEL PATH // import 'package:flux/features/attachments/ui/operation_files_section.dart'; class OperationFormScreen extends StatefulWidget { @@ -26,7 +23,6 @@ class OperationFormScreen extends StatefulWidget { class _OperationFormScreenState extends State { final _formKey = GlobalKey(); - // TEXT CONTROLLERS (Unici detentori di stato locale per evitare lag) final _referenceController = TextEditingController(); final _noteController = TextEditingController(); final _freeTextSubtypeController = TextEditingController(); @@ -43,40 +39,11 @@ 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 void initState() { super.initState(); - // Inizializziamo il form nel Cubit context.read().initOperationForm( existingOperation: widget.existingOperation, operationId: widget.operationId, @@ -91,7 +58,6 @@ class _OperationFormScreenState extends State { super.dispose(); } - // Sincronizza SOLO i testi liberi quando il Cubit ha caricato da DB void _syncTextControllers(OperationModel model) { if (_referenceController.text.isEmpty && model.reference.isNotEmpty) { _referenceController.text = model.reference; @@ -107,23 +73,20 @@ class _OperationFormScreenState extends State { _isInitialized = true; } - // --- LOGICA DI SALVATAGGIO --- void _saveOperation({required bool keepAdding}) { if (_formKey.currentState!.validate()) { final cubit = context.read(); final currentOperation = cubit.state.currentOperation!; - // 1. "Travasiamo" i testi liberi dai controller al Modello prima di salvare final operationToSave = currentOperation.copyWith( reference: _referenceController.text, note: _noteController.text, - // subtype: currentOperation.type == 'Custom' ? _customSubtypeController.text : currentOperation.subtype, // <-- Scommenta quando aggiungi subtype + subtype: ['Entertainment', 'Custom'].contains(currentOperation.type) + ? _freeTextSubtypeController.text + : currentOperation.subtype, ); - // 2. Aggiorniamo il Cubit con i testi cubit.initOperationForm(existingOperation: operationToSave); - - // 3. Salviamo! cubit.saveCurrentOperation( targetStatus: OperationStatus.ok, shouldPop: !keepAdding, @@ -131,165 +94,6 @@ class _OperationFormScreenState extends State { } } - // --- MODALE SELEZIONE CLIENTE --- - void _showCustomerModal() { - String currentSearchQuery = ''; - - showModalBottomSheet( - context: context, - isScrollControlled: true, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(16)), - ), - builder: (modalContext) { - return DraggableScrollableSheet( - initialChildSize: 0.8, - minChildSize: 0.5, - maxChildSize: 0.95, - expand: false, - builder: (_, scrollController) { - return Column( - children: [ - // Header - Padding( - padding: const EdgeInsets.all(16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Seleziona Cliente', - style: Theme.of(context).textTheme.titleLarge, - ), - IconButton( - icon: const Icon(Icons.close), - onPressed: () => Navigator.pop(modalContext), - ), - ], - ), - ), - // Barra di Ricerca - 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), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - onChanged: (query) { - currentSearchQuery = query; - context.read().searchCustomers(query); - }, - ), - ), - // Pulsante Nuovo Cliente - Padding( - padding: const EdgeInsets.all(16.0), - child: ElevatedButton.icon( - style: ElevatedButton.styleFrom( - minimumSize: const Size.fromHeight(48), - ), - icon: const Icon(Icons.person_add), - label: const Text('Crea 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); - } - } - }, - ), - ), - const Divider(), - // Lista Clienti dal Bloc - Expanded( - child: BlocBuilder( - builder: (context, state) { - if (state.status == CustomersStatus.loading) { - return const Center(child: CircularProgressIndicator()); - } - if (state.customers.isEmpty) { - return const Center( - child: Text( - 'Nessun cliente trovato.', - style: TextStyle(color: Colors.grey), - ), - ); - } - - return ListView.builder( - controller: scrollController, - itemCount: state.customers.length, - itemBuilder: (context, index) { - final customer = state.customers[index]; - return ListTile( - leading: CircleAvatar( - child: Text( - customer.name.substring(0, 1).toUpperCase(), - ), - ), - title: Text( - customer.name, - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ), - subtitle: Text( - '${customer.phoneNumber} • ${customer.email}', - ), - onTap: () { - // Aggiorniamo il form tramite il Cubit delle operazioni - context - .read() - .updateOperationFields( - customerId: customer.id, // customer.id - customerDisplayName: - customer.name, // customer.name - ); - Navigator.pop(modalContext); - }, - ); - }, - ); - }, - ), - ), - ], - ); - }, - ); - }, - ); - } - @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -299,7 +103,6 @@ class _OperationFormScreenState extends State { previous.status != current.status || previous.currentOperation?.id != current.currentOperation?.id, listener: (context, state) { - // Sincronizzazione iniziale if (state.status == OperationsStatus.ready && state.currentOperation != null && !_isInitialized) { @@ -315,9 +118,6 @@ class _OperationFormScreenState extends State { content: Text('Servizio aggiunto! Inserisci il prossimo.'), ), ); - // Ripuliamo SOLO i testi liberi (il Cubit gestisce già i suoi reset) - _referenceController.clear(); - _noteController.clear(); _freeTextSubtypeController.clear(); } else if (state.status == OperationsStatus.failure) { ScaffoldMessenger.of(context).showSnackBar( @@ -329,7 +129,6 @@ class _OperationFormScreenState extends State { } }, builder: (context, state) { - // Loader iniziale if (!_isInitialized && (widget.operationId != null || widget.existingOperation != null) && state.status == OperationsStatus.loading) { @@ -351,9 +150,7 @@ class _OperationFormScreenState extends State { child: LayoutBuilder( builder: (context, constraints) { final isDesktop = constraints.maxWidth > 900; - if (isDesktop) { - // --- LAYOUT DESKTOP --- return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -375,7 +172,6 @@ class _OperationFormScreenState extends State { ], ); } else { - // --- LAYOUT MOBILE --- return SingleChildScrollView( padding: const EdgeInsets.all(16.0), child: Column( @@ -392,7 +188,6 @@ class _OperationFormScreenState extends State { }, ), ), - // --- LA CASSA --- bottomNavigationBar: SafeArea( child: Padding( padding: const EdgeInsets.all(16.0), @@ -438,8 +233,6 @@ class _OperationFormScreenState extends State { ); } - // --- COSTRUTTORI UI COMPONENTI --- - Widget _buildMainFormContent(ThemeData theme, OperationsState state) { final currentOp = state.currentOperation; final currentType = currentOp?.type ?? 'AL'; @@ -447,9 +240,8 @@ class _OperationFormScreenState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // --- BLOCCO 1: CONTESTO --- _buildSectionTitle('Cliente & Riferimento'), - _buildCustomerSelector(currentOp), + CustomerSection(currentOp: currentOp), const SizedBox(height: 16), TextFormField( controller: _referenceController, @@ -460,7 +252,6 @@ class _OperationFormScreenState extends State { ), const Divider(height: 32), - // --- BLOCCO 2: TIPO DI OPERAZIONE --- _buildSectionTitle('Cosa stiamo facendo?'), Wrap( spacing: 8.0, @@ -471,7 +262,6 @@ class _OperationFormScreenState extends State { selected: currentType == type, onSelected: (selected) { if (selected) { - // Diciamo al Cubit di cambiare tipo e spianare i campi dipendenti context.read().setTypeWithSmartDefault(type); } }, @@ -480,155 +270,13 @@ class _OperationFormScreenState extends State { ), const Divider(height: 32), - // --- BLOCCO 3: DETTAGLI REATTIVI --- _buildSectionTitle('Dettagli Servizio'), - - // PROVIDER (Mostrato quasi sempre) - ListTile( - title: const Text('Seleziona Gestore'), - subtitle: Text( - (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: () { - _showProviderModal(currentType); - }, + DetailsSection( + currentOp: currentOp, + currentType: currentType, + freeTextSubtypeController: _freeTextSubtypeController, + durationQuickPicks: _buildDurationQuickPicks(currentOp), ), - const SizedBox(height: 16), - - // 1. SCENARIO ENERGY (Dropdown Fisso) - if (currentType == 'Energy') ...[ - DropdownButtonFormField( - initialValue: - (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) { - if (val != null) { - context.read().updateOperationFields( - subtype: val, - ); - } - }, - ), - const SizedBox(height: 16), - ], - - // 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: _freeTextSubtypeController, - decoration: InputDecoration( - labelText: currentType == 'Entertainment' - ? 'Piattaforma (es. Netflix, DAZN, Spotify...)' - : 'Specifica il servizio (es. Monopattino)', - ), - ), - const SizedBox(height: 16), - ], - - // 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 Effettiva'), - subtitle: Text( - 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_month, color: Colors.blue), - tileColor: Colors.blue.withValues(alpha: 0.05), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - side: const BorderSide(color: Colors.blue, width: 0.5), - ), - onTap: () async { - final OperationsCubit operationsCubit = context - .read(); - final date = await showDatePicker( - context: context, - initialDate: - currentOp?.expirationDate ?? - DateTime.now().add(const Duration(days: 365)), - firstDate: DateTime.now(), - lastDate: DateTime.now().add(const Duration(days: 3650)), - ); - if (date != null) { - operationsCubit.updateOperationFields(expirationDate: date); - } - }, - ), - const SizedBox(height: 16), - ], // QUANTITÀ Row( @@ -662,7 +310,6 @@ class _OperationFormScreenState extends State { ), const Divider(height: 32), - // --- BLOCCO 5: ALLEGATI --- _buildSectionTitle('Documenti & Foto'), const Center( child: Text( @@ -676,7 +323,6 @@ class _OperationFormScreenState extends State { Widget _buildDurationQuickPicks(OperationModel? currentOp) { final durations = [3, 6, 12, 24, 30, 36, 48]; - return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -700,13 +346,12 @@ class _OperationFormScreenState extends State { 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, + expirationDate: DateTime( + now.year, + now.month + months, + now.day, + ), ); }, ), @@ -733,22 +378,19 @@ class _OperationFormScreenState extends State { border: OutlineInputBorder(), ), ); - - if (isDesktop) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - title, - const SizedBox(height: 8), - Expanded(child: noteField), - ], - ); - } else { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [title, const SizedBox(height: 8), noteField], - ); - } + return isDesktop + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + title, + const SizedBox(height: 8), + Expanded(child: noteField), + ], + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [title, const SizedBox(height: 8), noteField], + ); } Widget _buildSectionTitle(String title) { @@ -762,284 +404,4 @@ class _OperationFormScreenState extends State { ), ); } - - Widget _buildCustomerSelector(OperationModel? currentOp) { - final hasCustomer = - currentOp?.customerId != null && currentOp!.customerId!.isNotEmpty; - - return InkWell( - onTap: _showCustomerModal, - child: Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - border: Border.all(color: Theme.of(context).colorScheme.primary), - borderRadius: BorderRadius.circular(8), - color: Theme.of( - context, - ).colorScheme.primaryContainer.withValues(alpha: 0.2), - ), - child: Row( - children: [ - const Icon(Icons.person), - const SizedBox(width: 12), - Expanded( - child: Text( - hasCustomer - ? currentOp.customerDisplayName ?? '' - : 'Seleziona Cliente *', - style: TextStyle( - fontWeight: hasCustomer ? FontWeight.bold : FontWeight.normal, - color: hasCustomer ? null : Colors.grey, - ), - ), - ), - const Icon(Icons.search), - ], - ), - ), - ); - } - - 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: deviceModel.id, - modelDisplayName: deviceModel.nameWithBrand, - ); - Navigator.pop(modalContext); - }, - ); - }, - ); - }, - ), - ), - ], - ); - }, - ); - }, - ); - } } diff --git a/lib/features/operations/ui/widgets/customer_section.dart b/lib/features/operations/ui/widgets/customer_section.dart new file mode 100644 index 0000000..88ec939 --- /dev/null +++ b/lib/features/operations/ui/widgets/customer_section.dart @@ -0,0 +1,222 @@ +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/operations/blocs/operations_cubit.dart'; +import 'package:flux/features/operations/models/operation_model.dart'; + +class CustomerSection extends StatelessWidget { + final OperationModel? currentOp; + const CustomerSection({super.key, required this.currentOp}); + + @override + Widget build(BuildContext context) { + final hasCustomer = + currentOp?.customerId != null && currentOp!.customerId!.isNotEmpty; + final theme = Theme.of(context); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 12.0), + child: Text( + 'Cliente', + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ), + InkWell( + onTap: () => _showCustomerModal(context), // Passiamo il context! + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + border: Border.all(color: theme.colorScheme.primary), + borderRadius: BorderRadius.circular(8), + color: theme.colorScheme.primaryContainer.withValues(alpha: 0.2), + ), + child: Row( + children: [ + const Icon(Icons.person), + const SizedBox(width: 12), + Expanded( + child: Text( + hasCustomer + ? currentOp!.customerDisplayName! + : 'Seleziona Cliente *', + style: TextStyle( + fontWeight: hasCustomer + ? FontWeight.bold + : FontWeight.normal, + color: hasCustomer ? null : Colors.grey, + ), + ), + ), + const Icon(Icons.search), + ], + ), + ), + ), + ], + ); + } + + // --- MODALE SELEZIONE CLIENTE --- + void _showCustomerModal(BuildContext context) { + String currentSearchQuery = ''; + + showModalBottomSheet( + context: context, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + builder: (modalContext) { + return DraggableScrollableSheet( + initialChildSize: 0.8, + minChildSize: 0.5, + maxChildSize: 0.95, + expand: false, + builder: (_, scrollController) { + return Column( + children: [ + // Header + Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Seleziona Cliente', + style: Theme.of(context).textTheme.titleLarge, + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.pop(modalContext), + ), + ], + ), + ), + // Barra di Ricerca + 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), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onChanged: (query) { + currentSearchQuery = query; + context.read().searchCustomers(query); + }, + ), + ), + // Pulsante Nuovo Cliente + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton.icon( + style: ElevatedButton.styleFrom( + minimumSize: const Size.fromHeight(48), + ), + icon: const Icon(Icons.person_add), + label: const Text('Crea 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); + } + } + }, + ), + ), + const Divider(), + // Lista Clienti dal Bloc + Expanded( + child: BlocBuilder( + builder: (context, state) { + if (state.status == CustomersStatus.loading) { + return const Center(child: CircularProgressIndicator()); + } + if (state.customers.isEmpty) { + return const Center( + child: Text( + 'Nessun cliente trovato.', + style: TextStyle(color: Colors.grey), + ), + ); + } + + return ListView.builder( + controller: scrollController, + itemCount: state.customers.length, + itemBuilder: (context, index) { + final customer = state.customers[index]; + return ListTile( + leading: CircleAvatar( + child: Text( + customer.name.substring(0, 1).toUpperCase(), + ), + ), + title: Text( + customer.name, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + subtitle: Text( + '${customer.phoneNumber} • ${customer.email}', + ), + onTap: () { + // Aggiorniamo il form tramite il Cubit delle operazioni + context + .read() + .updateOperationFields( + customerId: customer.id, // customer.id + customerDisplayName: + customer.name, // customer.name + ); + Navigator.pop(modalContext); + }, + ); + }, + ); + }, + ), + ), + ], + ); + }, + ); + }, + ); + } +} diff --git a/lib/features/operations/ui/widgets/details_section.dart b/lib/features/operations/ui/widgets/details_section.dart new file mode 100644 index 0000000..343db76 --- /dev/null +++ b/lib/features/operations/ui/widgets/details_section.dart @@ -0,0 +1,413 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.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'; + +class DetailsSection extends StatelessWidget { + final OperationModel? currentOp; + final String currentType; + final TextEditingController freeTextSubtypeController; + final Widget durationQuickPicks; + + const DetailsSection({ + super.key, + required this.currentOp, + required this.currentType, + required this.freeTextSubtypeController, + required this.durationQuickPicks, + }); + + bool _doesProviderMatchOperationType(dynamic provider, String operationType) { + if (operationType == 'Custom') return true; + switch (operationType) { + case 'AL': + case 'MNP': + return provider.mobile == true; + case 'NIP': + return provider.landline == true; + case 'UNICA': + return provider.landline == true || provider.mobile == true; + case 'Energy': + return provider.energy == true; + case 'Fin': + return provider.financing == true; + case 'Entertainment': + return provider.entertainment == true; + case 'TELEPASS': + return provider.telepass == true; + default: + return true; + } + } + + void _showProviderModal(BuildContext context, String operationType) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + builder: (modalContext) { + return DraggableScrollableSheet( + initialChildSize: 0.5, + 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( + builder: (context, state) { + if (state.isLoading) { + return const Center(child: CircularProgressIndicator()); + } + + final allProviders = state.activeProviders; + final filteredProviders = allProviders + .where( + (p) => _doesProviderMatchOperationType( + p, + operationType, + ), + ) + .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.name, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + onTap: () { + context + .read() + .updateOperationFields( + providerId: provider.id, + providerDisplayName: provider.name, + ); + Navigator.pop(modalContext); + }, + ); + }, + ); + }, + ), + ), + ], + ); + }, + ); + }, + ); + } + + void _showModelModal(BuildContext context) { + 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 = context.read(); + final existingBrands = context + .read() + .state + .brands; + + final newModel = await showDialog( + context: context, + builder: (dialogContext) { + return BlocProvider.value( + value: context.read(), + child: QuickProductDialog( + existingBrands: existingBrands, + ), + ); + }, + ); + + if (newModel != null) { + operationsCubit.updateOperationFields( + modelId: newModel.id, + modelDisplayName: newModel.nameWithBrand, + ); + if (context.mounted) Navigator.pop(modalContext); + } + }, + ), + ), + const Divider(), + Expanded( + child: BlocBuilder( + builder: (context, state) { + return ListView.builder( + controller: scrollController, + itemCount: 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: deviceModel.id, + modelDisplayName: deviceModel.nameWithBrand, + ); + Navigator.pop(modalContext); + }, + ); + }, + ); + }, + ), + ), + ], + ); + }, + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // PROVIDER (Mostrato quasi sempre) + ListTile( + title: const Text('Seleziona Gestore'), + subtitle: Text( + (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: () => _showProviderModal(context, currentType), + ), + const SizedBox(height: 16), + + // 1. SCENARIO ENERGY (Dropdown Fisso) + if (currentType == 'Energy') ...[ + DropdownButtonFormField( + initialValue: + (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) { + if (val != null) { + context.read().updateOperationFields( + subtype: val, + ); + } + }, + ), + const SizedBox(height: 16), + ], + + // 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(context), + ), + const SizedBox(height: 16), + ], + + // 3. SCENARIO ENTERTAINMENT O CUSTOM (Testo libero) + if (currentType == 'Entertainment' || currentType == 'Custom') ...[ + TextFormField( + controller: freeTextSubtypeController, + decoration: InputDecoration( + labelText: currentType == 'Entertainment' + ? 'Piattaforma (es. Netflix, DAZN, Spotify...)' + : 'Specifica il servizio (es. Monopattino)', + ), + ), + const SizedBox(height: 16), + ], + + // SCADENZA (Reattivo per tipi complessi) + if ([ + 'Energy', + 'Fin', + 'Entertainment', + 'Custom', + ].contains(currentType)) ...[ + const SizedBox(height: 8), + durationQuickPicks, // Passiamo i chips dall'esterno + const SizedBox(height: 16), + ListTile( + title: const Text('Data di Scadenza Effettiva'), + subtitle: Text( + 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_month, color: Colors.blue), + tileColor: Colors.blue.withValues(alpha: 0.05), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: const BorderSide(color: Colors.blue, width: 0.5), + ), + onTap: () async { + final date = await showDatePicker( + context: context, + initialDate: + currentOp?.expirationDate ?? + DateTime.now().add(const Duration(days: 365)), + firstDate: DateTime.now(), + lastDate: DateTime.now().add(const Duration(days: 3650)), + ); + if (date != null && context.mounted) { + context.read().updateOperationFields( + expirationDate: date, + ); + } + }, + ), + const SizedBox(height: 16), + ], + ], + ); + } +}