From 67e8b8b6548b1d15e02adaa19376346ce84034a6 Mon Sep 17 00:00:00 2001 From: Mark M2 Macbook Date: Sat, 2 May 2026 12:19:04 +0200 Subject: [PATCH] maremma maiala impestata, buonissima base dopo ultra refactor Co-authored-by: Copilot --- lib/core/routes/app_router.dart | 5 +- ...stomer_cubit.dart => customers_cubit.dart} | 28 +- ...stomer_state.dart => customers_state.dart} | 18 +- .../customers/ui/customer_search_sheet.dart | 202 ------ .../customers/ui/customers_content.dart | 14 +- .../customers/ui/quick_customer_dialog.dart | 18 +- .../operations/blocs/operations_cubit.dart | 32 +- .../models/energy_operation_model.dart | 72 -- .../models/entertainment_operation_model.dart | 77 --- .../models/fin_operation_model.dart | 63 -- .../operations/models/operation_model.dart | 7 + .../operations/ui/operation_form_screen.dart | 624 ++++++++++++++++++ .../ui/operation_form_screen/action_card.dart | 85 --- .../attachment_section.dart | 376 ----------- .../customer_section.dart | 96 --- .../energy_operation_dialog.dart | 417 ------------ .../entertainment_operation_card.dart | 393 ----------- .../finance_operation_dialog.dart | 479 -------------- .../general_info_section.dart | 65 -- .../ui/operation_form_screen/int_dialogs.dart | 158 ----- .../operation_form_screen.dart | 201 ------ .../operation_mobile_upload_screen.dart | 2 +- lib/main.dart | 4 +- 23 files changed, 706 insertions(+), 2730 deletions(-) rename lib/features/customers/blocs/{customer_cubit.dart => customers_cubit.dart} (84%) rename lib/features/customers/blocs/{customer_state.dart => customers_state.dart} (72%) delete mode 100644 lib/features/customers/ui/customer_search_sheet.dart delete mode 100644 lib/features/operations/models/energy_operation_model.dart delete mode 100644 lib/features/operations/models/entertainment_operation_model.dart delete mode 100644 lib/features/operations/models/fin_operation_model.dart create mode 100644 lib/features/operations/ui/operation_form_screen.dart delete mode 100644 lib/features/operations/ui/operation_form_screen/action_card.dart delete mode 100644 lib/features/operations/ui/operation_form_screen/attachment_section.dart delete mode 100644 lib/features/operations/ui/operation_form_screen/customer_section.dart delete mode 100644 lib/features/operations/ui/operation_form_screen/energy_operation_dialog.dart delete mode 100644 lib/features/operations/ui/operation_form_screen/entertainment_operation_card.dart delete mode 100644 lib/features/operations/ui/operation_form_screen/finance_operation_dialog.dart delete mode 100644 lib/features/operations/ui/operation_form_screen/general_info_section.dart delete mode 100644 lib/features/operations/ui/operation_form_screen/int_dialogs.dart delete mode 100644 lib/features/operations/ui/operation_form_screen/operation_form_screen.dart rename lib/features/operations/ui/{operation_form_screen => }/operation_mobile_upload_screen.dart (100%) diff --git a/lib/core/routes/app_router.dart b/lib/core/routes/app_router.dart index 087125a..c577ed2 100644 --- a/lib/core/routes/app_router.dart +++ b/lib/core/routes/app_router.dart @@ -12,7 +12,6 @@ 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/latest_store_operations/bloc/latest_store_operations_bloc.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/ui/products_screen.dart'; @@ -23,8 +22,8 @@ import 'package:flux/features/onboarding/blocs/onboarding_cubit.dart'; import 'package:flux/features/onboarding/ui/onboarding_screen.dart'; import 'package:flux/features/operations/blocs/operation_files_bloc.dart'; import 'package:flux/features/operations/models/operation_model.dart'; -import 'package:flux/features/operations/ui/operation_form_screen/operation_form_screen.dart'; -import 'package:flux/features/operations/ui/operation_form_screen/operation_mobile_upload_screen.dart'; +import 'package:flux/features/operations/ui/operation_form_screen.dart'; +import 'package:flux/features/operations/ui/operation_mobile_upload_screen.dart'; import 'package:flux/features/operations/ui/operations_screen.dart'; import 'package:get_it/get_it.dart'; import 'package:go_router/go_router.dart'; diff --git a/lib/features/customers/blocs/customer_cubit.dart b/lib/features/customers/blocs/customers_cubit.dart similarity index 84% rename from lib/features/customers/blocs/customer_cubit.dart rename to lib/features/customers/blocs/customers_cubit.dart index fba9b4d..44e58d5 100644 --- a/lib/features/customers/blocs/customer_cubit.dart +++ b/lib/features/customers/blocs/customers_cubit.dart @@ -6,31 +6,31 @@ import 'package:flux/features/customers/data/customer_repository.dart'; import 'package:flux/features/customers/models/customer_model.dart'; import 'package:get_it/get_it.dart'; -part 'customer_state.dart'; +part 'customers_state.dart'; -class CustomerCubit extends Cubit { +class CustomersCubit extends Cubit { final CustomerRepository _repository = GetIt.I(); final SessionCubit _sessionCubit = GetIt.I(); // Variabile per gestire il debounce della ricerca Timer? _searchDebounce; - CustomerCubit() : super(const CustomerState()); + CustomersCubit() : super(const CustomersState()); // --- LETTURA --- Future loadCustomers() async { - emit(state.copyWith(status: CustomerStatus.loading)); + emit(state.copyWith(status: CustomersStatus.loading)); try { final customers = await _repository.getCustomers( _sessionCubit.state.company!.id!, ); emit( - state.copyWith(status: CustomerStatus.success, customers: customers), + state.copyWith(status: CustomersStatus.success, customers: customers), ); } catch (e) { emit( state.copyWith( - status: CustomerStatus.failure, + status: CustomersStatus.failure, errorMessage: e.toString(), ), ); @@ -39,7 +39,7 @@ class CustomerCubit extends Cubit { // --- CREAZIONE --- Future createCustomer(CustomerModel customer) async { - emit(state.copyWith(status: CustomerStatus.loading)); + emit(state.copyWith(status: CustomersStatus.loading)); try { final newCustomer = await _repository.saveCustomer(customer); @@ -49,7 +49,7 @@ class CustomerCubit extends Cubit { emit( state.copyWith( - status: CustomerStatus.success, + status: CustomersStatus.success, customers: updatedList, lastCreatedCustomer: newCustomer, ), @@ -57,7 +57,7 @@ class CustomerCubit extends Cubit { } catch (e) { emit( state.copyWith( - status: CustomerStatus.failure, + status: CustomersStatus.failure, errorMessage: e.toString(), ), ); @@ -66,7 +66,7 @@ class CustomerCubit extends Cubit { // --- AGGIORNAMENTO --- Future updateCustomer(CustomerModel customer) async { - emit(state.copyWith(status: CustomerStatus.loading)); + emit(state.copyWith(status: CustomersStatus.loading)); try { final updatedCustomer = await _repository.updateCustomer(customer); @@ -79,7 +79,7 @@ class CustomerCubit extends Cubit { emit( state.copyWith( - status: CustomerStatus.success, + status: CustomersStatus.success, customers: updatedList, lastCreatedCustomer: updatedCustomer, // Utile se modifichi un cliente appena creato @@ -88,7 +88,7 @@ class CustomerCubit extends Cubit { } catch (e) { emit( state.copyWith( - status: CustomerStatus.failure, + status: CustomersStatus.failure, errorMessage: e.toString(), ), ); @@ -115,12 +115,12 @@ class CustomerCubit extends Cubit { query, ); emit( - state.copyWith(status: CustomerStatus.success, customers: results), + state.copyWith(status: CustomersStatus.success, customers: results), ); } catch (e) { emit( state.copyWith( - status: CustomerStatus.failure, + status: CustomersStatus.failure, errorMessage: e.toString(), ), ); diff --git a/lib/features/customers/blocs/customer_state.dart b/lib/features/customers/blocs/customers_state.dart similarity index 72% rename from lib/features/customers/blocs/customer_state.dart rename to lib/features/customers/blocs/customers_state.dart index da5ffec..453aaf7 100644 --- a/lib/features/customers/blocs/customer_state.dart +++ b/lib/features/customers/blocs/customers_state.dart @@ -1,6 +1,6 @@ -part of 'customer_cubit.dart'; +part of 'customers_cubit.dart'; -enum CustomerStatus { +enum CustomersStatus { initial, loading, filesLoading, @@ -9,26 +9,26 @@ enum CustomerStatus { failure, } -class CustomerState extends Equatable { - final CustomerStatus status; +class CustomersState extends Equatable { + final CustomersStatus status; final List customers; final CustomerModel? lastCreatedCustomer; final String? errorMessage; - const CustomerState({ - this.status = CustomerStatus.initial, + const CustomersState({ + this.status = CustomersStatus.initial, this.customers = const [], this.lastCreatedCustomer, this.errorMessage, }); - CustomerState copyWith({ - CustomerStatus? status, + CustomersState copyWith({ + CustomersStatus? status, List? customers, CustomerModel? lastCreatedCustomer, String? errorMessage, }) { - return CustomerState( + return CustomersState( status: status ?? this.status, customers: customers ?? this.customers, lastCreatedCustomer: lastCreatedCustomer ?? this.lastCreatedCustomer, diff --git a/lib/features/customers/ui/customer_search_sheet.dart b/lib/features/customers/ui/customer_search_sheet.dart deleted file mode 100644 index d0a6bcc..0000000 --- a/lib/features/customers/ui/customer_search_sheet.dart +++ /dev/null @@ -1,202 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/features/customers/blocs/customer_cubit.dart'; -import 'package:flux/features/customers/models/customer_model.dart'; -import 'package:flux/features/customers/ui/quick_customer_dialog.dart'; -import 'package:flux/features/operations/blocs/operations_cubit.dart'; - -class CustomerSearchSheet extends StatefulWidget { - const CustomerSearchSheet({super.key}); - - @override - State createState() => _CustomerSearchSheetState(); -} - -class _CustomerSearchSheetState extends State { - final TextEditingController _searchController = TextEditingController(); - - @override - void initState() { - super.initState(); - context.read().loadCustomers(); - } - - @override - void dispose() { - _searchController.dispose(); - super.dispose(); - } - - void _onSearchChanged(String query) { - context.read().searchCustomers(query); - } - - @override - Widget build(BuildContext context) { - return ConstrainedBox( - constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height * 0.85, - ), - child: Padding( - padding: const EdgeInsets.all(24.0), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // --- HEADER --- - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - "Trova Cliente", - style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold), - ), - IconButton( - icon: const Icon(Icons.close), - onPressed: () => Navigator.pop(context), - tooltip: "Chiudi", - ), - ], - ), - const SizedBox(height: 16), - - // --- BARRA DI RICERCA --- - TextField( - controller: _searchController, - decoration: InputDecoration( - hintText: "Cerca per nome, cognome o CF...", - prefixIcon: const Icon(Icons.search), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - ), - filled: true, - fillColor: Theme.of( - context, - ).colorScheme.surfaceContainerHighest.withValues(alpha: 0.3), - suffixIcon: IconButton( - icon: const Icon(Icons.clear), - onPressed: () { - _searchController.clear(); - _onSearchChanged(""); - }, - ), - ), - onChanged: _onSearchChanged, - ), - const SizedBox(height: 16), - - // --- TASTO NUOVO CLIENTE --- - SizedBox( - width: double.infinity, - child: IconButton( - icon: const Icon(Icons.person_add), - onPressed: () async { - final operationsCubit = context.read(); - // Apriamo la dialog passando la query attuale - final CustomerModel? nuovoCliente = await showDialog( - context: context, - builder: (context) => QuickCustomerDialog( - initialQuery: _searchController.text, - ), - ); - - if (nuovoCliente != null) { - operationsCubit.updateField( - customerId: nuovoCliente.id, - customerDisplayName: nuovoCliente.name, - ); - - setState(() { - _searchController.clear(); - }); - } - }, - ), - ), - const SizedBox(height: 24), - - // --- LISTA RISULTATI CON BLOC BUILDER --- - const Text( - "Risultati", - style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey), - ), - const SizedBox(height: 8), - - Expanded( - // AGGANCIO AL CUBIT REALE - child: BlocBuilder( - builder: (context, state) { - // 1. Stato di caricamento - if (state.status == CustomerStatus.loading) { - return const Center(child: CircularProgressIndicator()); - } - - // 2. Nessun risultato trovato - if (state.customers.isEmpty) { - return const Center( - child: Text( - "Nessun cliente trovato.\nProva a cambiare i termini di ricerca.", - textAlign: TextAlign.center, - style: TextStyle(color: Colors.grey), - ), - ); - } - - // 3. Mostriamo la lista vera - return ListView.separated( - itemCount: state.customers.length, - separatorBuilder: (context, index) => - const Divider(height: 1), - itemBuilder: (context, index) { - final customer = state.customers[index]; - // Assumo che il tuo CustomerModel abbia le proprietà name e surname. - // Adatta queste variabili al tuo modello reale! - final displayName = customer.name.trim(); - - return ListTile( - contentPadding: EdgeInsets.zero, - leading: CircleAvatar( - backgroundColor: Theme.of( - context, - ).colorScheme.primaryContainer, - foregroundColor: Theme.of( - context, - ).colorScheme.onPrimaryContainer, - // Mostra l'iniziale - child: Text( - displayName.isNotEmpty - ? displayName[0].toUpperCase() - : "?", - ), - ), - title: Text( - displayName, - style: const TextStyle(fontWeight: FontWeight.w500), - ), - subtitle: Text(customer.email), - trailing: const Icon( - Icons.check_circle_outline, - color: Colors.grey, - ), - onTap: () { - // Salviamo l'ID e il nome formattato nel form dei servizi - context.read().updateField( - customerId: customer.id, - customerDisplayName: displayName, - ); - - // Chiudiamo la modale - Navigator.pop(context); - }, - ); - }, - ); - }, - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/features/customers/ui/customers_content.dart b/lib/features/customers/ui/customers_content.dart index 7021922..c52a9a9 100644 --- a/lib/features/customers/ui/customers_content.dart +++ b/lib/features/customers/ui/customers_content.dart @@ -2,7 +2,7 @@ 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/theme/theme.dart'; -import 'package:flux/features/customers/blocs/customer_cubit.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_form.dart'; import 'package:go_router/go_router.dart'; @@ -26,14 +26,14 @@ class _CustomersContentState extends State { void _loadInitialCustomers() { final companyId = context.read().state.company?.id; if (companyId != null) { - context.read().loadCustomers(); + context.read().loadCustomers(); } } void _onSearch(String query) { final companyId = context.read().state.company?.id; if (companyId != null) { - context.read().searchCustomers(query); + context.read().searchCustomers(query); } } @@ -86,9 +86,9 @@ class _CustomersContentState extends State { // LISTA CLIENTI Expanded( - child: BlocBuilder( + child: BlocBuilder( builder: (context, state) { - if (state.status == CustomerStatus.loading && + if (state.status == CustomersStatus.loading && state.customers.isEmpty) { return const Center(child: CircularProgressIndicator()); } @@ -242,12 +242,12 @@ void openCustomerForm({ if (customer == null) { // CASO NUOVO: Iniettiamo il companyId e inviamo l'evento create - context.read().createCustomer( + context.read().createCustomer( customerFromForm.copyWith(companyId: companyId), ); } else { // CASO MODIFICA: L'ID e il companyId sono già nel modello - context.read().updateCustomer(customerFromForm); + context.read().updateCustomer(customerFromForm); } Navigator.pop(dialogContext); }, diff --git a/lib/features/customers/ui/quick_customer_dialog.dart b/lib/features/customers/ui/quick_customer_dialog.dart index 3082491..2137dee 100644 --- a/lib/features/customers/ui/quick_customer_dialog.dart +++ b/lib/features/customers/ui/quick_customer_dialog.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/features/customers/blocs/customer_cubit.dart'; +import 'package:flux/features/customers/blocs/customers_cubit.dart'; class QuickCustomerDialog extends StatefulWidget { final String initialQuery; @@ -42,13 +42,15 @@ class _QuickCustomerDialogState extends State { setState(() => _isLoading = true); // Chiamata al Cubit (aggiorna i parametri in base a come li hai definiti) - final newCustomer = await context.read().quickCreateCustomer( - name: _nameCtrl.text.trim(), - phone: _phoneCtrl.text.trim(), - // Aggiungi questi se li hai inseriti nel tuo CustomerCubit: - // email: _emailCtrl.text.trim(), - // note: _noteCtrl.text.trim(), - ); + final newCustomer = await context + .read() + .quickCreateCustomer( + name: _nameCtrl.text.trim(), + phone: _phoneCtrl.text.trim(), + // Aggiungi questi se li hai inseriti nel tuo CustomerCubit: + // email: _emailCtrl.text.trim(), + // note: _noteCtrl.text.trim(), + ); setState(() => _isLoading = false); diff --git a/lib/features/operations/blocs/operations_cubit.dart b/lib/features/operations/blocs/operations_cubit.dart index cfdf1a6..536f9eb 100644 --- a/lib/features/operations/blocs/operations_cubit.dart +++ b/lib/features/operations/blocs/operations_cubit.dart @@ -210,12 +210,40 @@ class OperationsCubit extends Cubit { .toList(); } - void updateField({String? customerId, String? customerDisplayName}) { + // --- GESTIONE DELLO STATO DEL FORM IN TEMPO REALE --- + void updateOperationFields({ + String? customerId, + String? customerDisplayName, + String? type, + String? providerId, + String? subtype, + DateTime? expirationDate, + int? quantity, + // Aggiungiamo questi flag per forzare la pulizia dei campi quando cambi tipo + bool clearProvider = false, + bool clearType = false, + bool clearSubtype = false, + bool clearExpiration = false, + }) { if (state.currentOperation == null) return; - final updated = state.currentOperation!.copyWith( + + final current = state.currentOperation!; + + // Creiamo il modello aggiornato + // ATTENZIONE: adatta questa logica in base a come è scritto il tuo copyWith! + 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, ); + emit(state.copyWith(currentOperation: updated)); } } diff --git a/lib/features/operations/models/energy_operation_model.dart b/lib/features/operations/models/energy_operation_model.dart deleted file mode 100644 index 817d76e..0000000 --- a/lib/features/operations/models/energy_operation_model.dart +++ /dev/null @@ -1,72 +0,0 @@ -import 'package:equatable/equatable.dart'; - -enum EnergyType { luce, gas } // Mappa il tuo public.energy_type - -class EnergyOperationModel extends Equatable { - final String? id; - final DateTime? createdAt; - final EnergyType type; - final DateTime expiration; - final String providerId; - final String? operationId; - - const EnergyOperationModel({ - this.id, - this.createdAt, - required this.type, - required this.expiration, - required this.providerId, - this.operationId, - }); - - EnergyOperationModel copyWith({ - String? id, - DateTime? createdAt, - EnergyType? type, - DateTime? expiration, - String? providerId, - String? operationId, - }) { - return EnergyOperationModel( - id: id ?? this.id, - createdAt: createdAt ?? this.createdAt, - type: type ?? this.type, - expiration: expiration ?? this.expiration, - providerId: providerId ?? this.providerId, - operationId: operationId ?? this.operationId, - ); - } - - @override - List get props => [ - id, - createdAt, - type, - expiration, - providerId, - operationId, - ]; - - factory EnergyOperationModel.fromMap(Map map) { - return EnergyOperationModel( - id: map['id'], - createdAt: map['created_at'] != null - ? DateTime.parse(map['created_at']) - : null, - type: map['type'] == 'gas' ? EnergyType.gas : EnergyType.luce, - expiration: DateTime.parse(map['expiration']), - providerId: map['provider_id'], - operationId: map['operation_id'], - ); - } - - Map toMap() { - return { - if (id != null) 'id': id, - 'type': type.name, // .name trasforma l'enum in 'luce' o 'gas' - 'expiration': expiration.toIso8601String(), - 'provider_id': providerId, - 'operation_id': operationId, - }; - } -} diff --git a/lib/features/operations/models/entertainment_operation_model.dart b/lib/features/operations/models/entertainment_operation_model.dart deleted file mode 100644 index 49930b3..0000000 --- a/lib/features/operations/models/entertainment_operation_model.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'package:equatable/equatable.dart'; - -class EntertainmentOperationModel extends Equatable { - final String? id; - final DateTime? createdAt; - final String type; // es. Sky, DAZN, ecc. - final bool constrained; // Vincolato? - final DateTime constrainExpiration; - final String? operationId; - final String? providerId; - - const EntertainmentOperationModel({ - this.id, - this.createdAt, - required this.type, - required this.constrained, - required this.constrainExpiration, - this.operationId, - this.providerId, - }); - - EntertainmentOperationModel copyWith({ - String? id, - DateTime? createdAt, - String? type, - bool? constrained, - DateTime? constrainExpiration, - String? operationId, - String? providerId, - }) { - return EntertainmentOperationModel( - id: id ?? this.id, - createdAt: createdAt ?? this.createdAt, - type: type ?? this.type, - constrained: constrained ?? this.constrained, - constrainExpiration: constrainExpiration ?? this.constrainExpiration, - operationId: operationId ?? this.operationId, - providerId: providerId ?? this.providerId, - ); - } - - @override - List get props => [ - id, - createdAt, - type, - constrained, - constrainExpiration, - operationId, - providerId, - ]; - - factory EntertainmentOperationModel.fromMap(Map map) { - return EntertainmentOperationModel( - id: map['id'], - createdAt: map['created_at'] != null - ? DateTime.parse(map['created_at']) - : null, - type: map['type'], - constrained: map['constrained'] ?? false, - constrainExpiration: DateTime.parse(map['constrain_expiration']), - operationId: map['operation_id'], - providerId: map['provider_id'], - ); - } - - Map toMap() { - return { - if (id != null) 'id': id, - 'type': type, - 'constrained': constrained, - 'constrain_expiration': constrainExpiration.toIso8601String(), - 'operation_id': operationId, - 'provider_id': providerId, - }; - } -} diff --git a/lib/features/operations/models/fin_operation_model.dart b/lib/features/operations/models/fin_operation_model.dart deleted file mode 100644 index d7bf513..0000000 --- a/lib/features/operations/models/fin_operation_model.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'package:equatable/equatable.dart'; - -class FinOperationModel extends Equatable { - final String? id; - final DateTime? createdAt; - final DateTime expiration; - final String? operationId; - final String? modelId; // FK verso model (es. iPhone, Samsung, ecc.) - final String? providerId; - - const FinOperationModel({ - this.id, - this.createdAt, - required this.expiration, - this.operationId, - this.modelId, - this.providerId, - }); - - FinOperationModel copyWith({ - String? id, - DateTime? createdAt, - DateTime? expiration, - String? operationId, - String? modelId, - String? providerId, - }) { - return FinOperationModel( - id: id ?? this.id, - createdAt: createdAt ?? this.createdAt, - expiration: expiration ?? this.expiration, - operationId: operationId ?? this.operationId, - modelId: modelId ?? this.modelId, - providerId: providerId ?? this.providerId, - ); - } - - @override - List get props => [id, createdAt, expiration, operationId, modelId]; - - factory FinOperationModel.fromMap(Map map) { - return FinOperationModel( - id: map['id'], - createdAt: map['created_at'] != null - ? DateTime.parse(map['created_at']) - : null, - expiration: DateTime.parse(map['expiration']), - operationId: map['operation_id'], - modelId: map['model_id'], - providerId: map['provider_id'], - ); - } - - Map toMap() { - return { - if (id != null) 'id': id, - 'expiration': expiration.toIso8601String(), - 'operation_id': operationId, - 'model_id': modelId, - 'provider_id': providerId, - }; - } -} diff --git a/lib/features/operations/models/operation_model.dart b/lib/features/operations/models/operation_model.dart index 7ffe874..727eb11 100644 --- a/lib/features/operations/models/operation_model.dart +++ b/lib/features/operations/models/operation_model.dart @@ -27,6 +27,7 @@ class OperationModel extends Equatable { final String? id; final DateTime? createdAt; final String type; + final String? subType; final String? providerId; final String? providerDisplayName; final String? modelId; @@ -55,6 +56,7 @@ class OperationModel extends Equatable { this.id, this.createdAt, this.type = '', + this.subType, this.providerId, this.providerDisplayName, this.modelId, @@ -82,6 +84,7 @@ class OperationModel extends Equatable { String? id, DateTime? createdAt, String? type, + String? subtype, String? providerId, String? providerDisplayName, String? modelId, @@ -107,6 +110,7 @@ class OperationModel extends Equatable { id: id ?? this.id, createdAt: createdAt ?? this.createdAt, type: type ?? this.type, + subType: subtype ?? this.subType, providerId: providerId ?? this.providerId, providerDisplayName: providerDisplayName ?? this.providerDisplayName, modelId: modelId ?? this.modelId, @@ -135,6 +139,7 @@ class OperationModel extends Equatable { id, createdAt, type, + subType, providerId, providerDisplayName, modelId, @@ -169,6 +174,7 @@ class OperationModel extends Equatable { ? 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? ?? '', @@ -206,6 +212,7 @@ class OperationModel extends Equatable { return { if (id != null) 'id': id, 'type': type, + 'sub_type': subType, 'provider_id': providerId, 'model_id': modelId, 'description': description, diff --git a/lib/features/operations/ui/operation_form_screen.dart b/lib/features/operations/ui/operation_form_screen.dart new file mode 100644 index 0000000..6da630a --- /dev/null +++ b/lib/features/operations/ui/operation_form_screen.dart @@ -0,0 +1,624 @@ +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/operations/blocs/operations_cubit.dart'; +import 'package:flux/features/operations/models/operation_model.dart'; +// import 'package:flux/features/attachments/ui/operation_files_section.dart'; + +class OperationFormScreen extends StatefulWidget { + final String? operationId; + final OperationModel? existingOperation; + + const OperationFormScreen({ + super.key, + this.operationId, + this.existingOperation, + }); + + @override + State createState() => _OperationFormScreenState(); +} + +class _OperationFormScreenState extends State { + final _formKey = GlobalKey(); + + // TEXT CONTROLLERS (Unici detentori di stato locale per evitare lag) + final _referenceController = TextEditingController(); + final _noteController = TextEditingController(); + final _customSubtypeController = TextEditingController(); + + final List _availableTypes = [ + 'AL', + 'MNP', + 'NIP', + 'UNICA', + 'TELEPASS', + 'Energy', + 'Fin', + 'Entertainment', + 'Custom', + ]; + + bool _isInitialized = false; + + @override + void initState() { + super.initState(); + // Inizializziamo il form nel Cubit + context.read().initOperationForm( + existingOperation: widget.existingOperation, + operationId: widget.operationId, + ); + } + + @override + void dispose() { + _referenceController.dispose(); + _noteController.dispose(); + _customSubtypeController.dispose(); + 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; + } + if (_noteController.text.isEmpty && model.note.isNotEmpty) { + _noteController.text = model.note; + } + _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 + ); + + // 2. Aggiorniamo il Cubit con i testi + cubit.initOperationForm(existingOperation: operationToSave); + + // 3. Salviamo! + cubit.saveCurrentOperation( + targetStatus: OperationStatus.ok, + shouldPop: !keepAdding, + ); + } + } + + // --- MODALE SELEZIONE CLIENTE --- + void _showCustomerModal() { + 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( + decoration: InputDecoration( + hintText: 'Cerca per nome, telefono o email...', + prefixIcon: const Icon(Icons.search), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onChanged: (query) { + // Evento di ricerca (usa debouncer nel cubit!) + // 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: () { + // Apri form nuovo cliente... + }, + ), + ), + const Divider(), + // Lista Clienti dal Bloc + Expanded( + child: BlocBuilder( + builder: (context, state) { + /* Decommenta e adatta al tuo CustomersState + 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: 10, // Sostituisci con state.customers.length + itemBuilder: (context, index) { + // final customer = state.customers[index]; + return ListTile( + leading: const CircleAvatar( + child: Icon(Icons.person), + ), + title: Text( + 'Cliente $index', + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), // Sostituisci con customer.name + subtitle: const Text( + '333 1234567', + ), // Sostituisci con customer.phoneNumber + onTap: () { + // Aggiorniamo il form tramite il Cubit delle operazioni + context + .read() + .updateOperationFields( + customerId: + 'id_del_cliente_$index', // customer.id + customerDisplayName: + 'Cliente $index', // customer.name + ); + Navigator.pop(modalContext); + }, + ); + }, + ); + }, + ), + ), + ], + ); + }, + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return BlocConsumer( + listenWhen: (previous, current) => + previous.status != current.status || + previous.currentOperation?.id != current.currentOperation?.id, + listener: (context, state) { + // Sincronizzazione iniziale + if (state.status == OperationsStatus.ready && + state.currentOperation != null && + !_isInitialized) { + _syncTextControllers(state.currentOperation!); + } + + if (state.status == OperationsStatus.saved) { + Navigator.of(context).pop(); + } else if (state.status == OperationsStatus.savedNoPop) { + context.read().prepareNextOperationInBatch(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Servizio aggiunto! Inserisci il prossimo.'), + ), + ); + // Ripuliamo SOLO i testi liberi (il Cubit gestisce già i suoi reset) + _referenceController.clear(); + _noteController.clear(); + _customSubtypeController.clear(); + } else if (state.status == OperationsStatus.failure) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.errorMessage ?? 'Errore'), + backgroundColor: theme.colorScheme.error, + ), + ); + } + }, + builder: (context, state) { + // Loader iniziale + if (!_isInitialized && + (widget.operationId != null || widget.existingOperation != null) && + state.status == OperationsStatus.loading) { + return const Scaffold( + body: Center(child: CircularProgressIndicator()), + ); + } + + return Scaffold( + appBar: AppBar( + title: Text( + state.currentOperation?.id == null + ? 'Nuova Pratica' + : 'Modifica Pratica', + ), + ), + body: Form( + key: _formKey, + child: LayoutBuilder( + builder: (context, constraints) { + final isDesktop = constraints.maxWidth > 900; + + if (isDesktop) { + // --- LAYOUT DESKTOP --- + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 7, + child: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: _buildMainFormContent(theme, state), + ), + ), + VerticalDivider(width: 1, color: theme.dividerColor), + Expanded( + flex: 3, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: _buildNotesSection(isDesktop: true), + ), + ), + ], + ); + } else { + // --- LAYOUT MOBILE --- + return SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildMainFormContent(theme, state), + const Divider(height: 32), + _buildNotesSection(isDesktop: false), + const SizedBox(height: 80), + ], + ), + ); + } + }, + ), + ), + // --- LA CASSA --- + bottomNavigationBar: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + Expanded( + flex: 1, + child: OutlinedButton( + onPressed: state.status == OperationsStatus.saving + ? null + : () => _saveOperation(keepAdding: true), + child: const Text( + 'Salva e Aggiungi Altro', + textAlign: TextAlign.center, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + flex: 1, + child: ElevatedButton( + onPressed: state.status == OperationsStatus.saving + ? null + : () => _saveOperation(keepAdding: false), + child: state.status == OperationsStatus.saving + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ) + : const Text('Salva ed Esci'), + ), + ), + ], + ), + ), + ), + ); + }, + ); + } + + // --- COSTRUTTORI UI COMPONENTI --- + + Widget _buildMainFormContent(ThemeData theme, OperationsState state) { + final currentOp = state.currentOperation; + final currentType = currentOp?.type ?? 'AL'; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // --- BLOCCO 1: CONTESTO --- + _buildSectionTitle('Cliente & Riferimento'), + _buildCustomerSelector(currentOp), + const SizedBox(height: 16), + TextFormField( + controller: _referenceController, + decoration: const InputDecoration( + labelText: 'Riferimento (es. numero di telefono, targa...)', + prefixIcon: Icon(Icons.tag), + ), + ), + const Divider(height: 32), + + // --- BLOCCO 2: TIPO DI OPERAZIONE --- + _buildSectionTitle('Cosa stiamo facendo?'), + Wrap( + spacing: 8.0, + runSpacing: 8.0, + children: _availableTypes.map((type) { + return ChoiceChip( + label: Text(type), + selected: currentType == type, + 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, + ); + } + }, + ); + }).toList(), + ), + const Divider(height: 32), + + // --- BLOCCO 3: DETTAGLI REATTIVI --- + _buildSectionTitle('Dettagli Servizio'), + + // PROVIDER (Mostrato quasi sempre) + ListTile( + title: const Text('Seleziona Gestore'), + subtitle: Text( + currentOp?.providerId ?? 'Nessun gestore selezionato', + ), // Adatta se hai displayName + trailing: const Icon(Icons.arrow_drop_down), + shape: RoundedRectangleBorder( + side: BorderSide(color: theme.dividerColor), + borderRadius: BorderRadius.circular(8), + ), + onTap: () { + // TODO: Modale o Dropdown Provider + }, + ), + const SizedBox(height: 16), + + // SOTTO-TIPO (Reattivo) + if (['Energy', 'Fin', 'Entertainment'].contains(currentType)) ...[ + DropdownButtonFormField( + value: + null, // Sostituisci con currentOp?.subtype quando lo aggiungi + decoration: const InputDecoration( + labelText: 'Dettaglio (es. Luce, Gas...)', + ), + items: [ + 'Luce', + 'Gas', + 'Dual', + ].map((s) => DropdownMenuItem(value: s, child: Text(s))).toList(), + onChanged: (val) { + // context.read().updateOperationFields(subtype: val); + }, + ), + const SizedBox(height: 16), + ], + + // SOTTO-TIPO CUSTOM (Reattivo) + if (currentType == 'Custom') ...[ + TextFormField( + controller: _customSubtypeController, + decoration: const InputDecoration( + labelText: 'Specifica il servizio (es. Monopattino)', + ), + ), + const SizedBox(height: 16), + ], + + // SCADENZA (Reattivo) + if ([ + 'Energy', + 'Fin', + 'Entertainment', + 'Custom', + ].contains(currentType)) ...[ + ListTile( + title: const Text('Data di Scadenza'), + subtitle: Text( + currentOp?.expirationDate?.toLocal().toString().split(' ')[0] ?? + 'Nessuna scadenza', + ), + trailing: const Icon(Icons.calendar_today), + shape: RoundedRectangleBorder( + side: BorderSide(color: theme.dividerColor), + borderRadius: BorderRadius.circular(8), + ), + onTap: () async { + final date = await showDatePicker( + context: context, + initialDate: DateTime.now().add(const Duration(days: 365)), + firstDate: DateTime.now(), + lastDate: DateTime.now().add(const Duration(days: 3650)), + ); + if (date != null) { + context.read().updateOperationFields( + expirationDate: date, + ); + } + }, + ), + const SizedBox(height: 16), + ], + + // QUANTITÀ + Row( + children: [ + const Text('Quantità: '), + IconButton( + icon: const Icon(Icons.remove), + onPressed: () { + final q = currentOp?.quantity ?? 1; + if (q > 1) + context.read().updateOperationFields( + quantity: q - 1, + ); + }, + ), + Text( + '${currentOp?.quantity ?? 1}', + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + IconButton( + icon: const Icon(Icons.add), + onPressed: () { + final q = currentOp?.quantity ?? 1; + context.read().updateOperationFields( + quantity: q + 1, + ); + }, + ), + ], + ), + const Divider(height: 32), + + // --- BLOCCO 5: ALLEGATI --- + _buildSectionTitle('Documenti & Foto'), + const Center( + child: Text( + "Widget File in arrivo...", + style: TextStyle(color: Colors.grey), + ), + ), + ], + ); + } + + Widget _buildNotesSection({required bool isDesktop}) { + final title = _buildSectionTitle('Note Interne'); + final noteField = TextFormField( + controller: _noteController, + keyboardType: TextInputType.multiline, + minLines: isDesktop ? null : 5, + maxLines: null, + expands: isDesktop, + textAlignVertical: TextAlignVertical.top, + decoration: const InputDecoration( + hintText: 'Incolla qui seriali, ICCID, IBAN, indirizzi...', + alignLabelWithHint: true, + 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], + ); + } + } + + Widget _buildSectionTitle(String title) { + return Padding( + padding: const EdgeInsets.only(bottom: 12.0), + child: Text( + title, + style: Theme.of( + context, + ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), + ), + ); + } + + 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), + ], + ), + ), + ); + } +} diff --git a/lib/features/operations/ui/operation_form_screen/action_card.dart b/lib/features/operations/ui/operation_form_screen/action_card.dart deleted file mode 100644 index 51f0b42..0000000 --- a/lib/features/operations/ui/operation_form_screen/action_card.dart +++ /dev/null @@ -1,85 +0,0 @@ -import 'package:flutter/material.dart'; - -class ActionCard extends StatelessWidget { - final String label; - final int count; - final IconData icon; - final Color color; - final VoidCallback onTap; - - const ActionCard({ - super.key, - required this.label, - required this.count, - required this.icon, - required this.color, - required this.onTap, - }); - - @override - Widget build(BuildContext context) { - final isActive = count > 0; - - return InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(12), - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - width: 110, // Larghezza fissa per avere una griglia ordinata - padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 8), - decoration: BoxDecoration( - color: isActive - ? color.withValues(alpha: 0.15) - : Theme.of(context).cardColor, - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: isActive ? color : Colors.grey.withValues(alpha: 0.3), - width: isActive ? 2 : 1, - ), - boxShadow: isActive - ? [ - BoxShadow( - color: color.withValues(alpha: 0.2), - blurRadius: 8, - spreadRadius: 1, - ), - ] - : [], - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(icon, color: isActive ? color : Colors.grey, size: 28), - const SizedBox(height: 8), - Text( - label, - style: TextStyle( - fontWeight: isActive ? FontWeight.bold : FontWeight.normal, - color: isActive ? color : Colors.grey.shade700, - ), - textAlign: TextAlign.center, - ), - if (isActive) ...[ - const SizedBox(height: 4), - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: color, - borderRadius: BorderRadius.circular(12), - ), - child: Text( - count.toString(), - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 12, - ), - ), - ), - ], - ], - ), - ), - ); - } -} diff --git a/lib/features/operations/ui/operation_form_screen/attachment_section.dart b/lib/features/operations/ui/operation_form_screen/attachment_section.dart deleted file mode 100644 index 7933045..0000000 --- a/lib/features/operations/ui/operation_form_screen/attachment_section.dart +++ /dev/null @@ -1,376 +0,0 @@ -import 'package:file_picker/file_picker.dart'; -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/utils/extensions.dart'; -import 'package:flux/core/widgets/image_viewer_widget.dart'; -import 'package:flux/core/widgets/pdf_viewer_widget.dart'; -import 'package:flux/core/widgets/qr_upload_dialog.dart'; -import 'package:flux/features/attachments/models/attachment_model.dart'; -import 'package:flux/features/operations/blocs/operation_files_bloc.dart'; -import 'package:flux/features/operations/blocs/operations_cubit.dart'; -import 'package:flux/features/operations/models/operation_model.dart'; - -class AttachmentsSection extends StatelessWidget { - const AttachmentsSection({super.key}); - - Future _pickFiles(BuildContext context) async { - // Usiamo withData: true fondamentale per avere i bytes e caricare su Supabase Storage - FilePickerResult? result = await FilePicker.pickFiles( - allowMultiple: true, - type: FileType.custom, - allowedExtensions: ['pdf', 'jpg', 'jpeg', 'png'], - withData: true, - ); - - if (result != null && context.mounted) { - context.read().add( - AddOperationFilesEvent(result.files), - ); - } - } - - @override - Widget build(BuildContext context) { - OperationFilesBloc operationFilesBloc = BlocProvider.of( - context, - ); - - return BlocListener( - listenWhen: (previous, current) => - previous.currentOperation?.id == null && - current.currentOperation?.id != null, - listener: (context, state) { - // FIGASSA! La pratica è stata salvata e ora ha un ID. - // Diciamo al Bloc dei file di agganciarsi al database. - final newId = state.currentOperation!.id!; - context.read().add(OperationsavedEvent(newId)); - }, - child: BlocBuilder( - builder: (context, state) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // --- HEADER SEZIONE --- - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "DOCUMENTI ALLEGATI", - style: TextStyle( - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.primary, - letterSpacing: 1.2, - ), - ), - Row( - children: [ - OutlinedButton.icon( - icon: const Icon(Icons.attach_file), - label: const Text("Aggiungi File"), - onPressed: () => _pickFiles(context), - ), - if (!context - .read() - .state - .isMobileDevice) ...[ - const SizedBox(width: 12), - ElevatedButton.icon( - onPressed: () => _handleGenerateQr(context), - icon: const Icon(Icons.qr_code), - label: const Text("GENERA QR"), - style: ElevatedButton.styleFrom( - backgroundColor: Theme.of( - context, - ).colorScheme.primary.withValues(alpha: 0.1), - foregroundColor: Theme.of( - context, - ).colorScheme.primary, - elevation: 0, - ), - ), - ], - ], - ), - ], - ), - const SizedBox(height: 12), - - // --- LISTA VUOTA --- - if (state.allFiles.isEmpty) // Furbata: usiamo allFiles.isEmpty! - Container( - width: double.infinity, - padding: const EdgeInsets.all(24), - decoration: BoxDecoration( - border: Border.all( - color: Colors.grey.shade300, - style: BorderStyle.solid, - ), - borderRadius: BorderRadius.circular(8), - color: Colors.grey.shade50, - ), - child: const Text( - "Nessun documento allegato alla bozza.", - textAlign: TextAlign.center, - style: TextStyle(color: Colors.grey), - ), - ) - // --- LISTA PIENA --- - else ...[ - ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: state.allFiles.length, - itemBuilder: (context, index) { - final file = state.allFiles[index]; - final sizeMb = (file.fileSize / (1024 * 1024)) - .toStringAsFixed(2); - final isPdf = file.extension.toLowerCase() == 'pdf'; - final isSelected = state.selectedFiles.contains(file); - - return GestureDetector( - onTap: () => operationFilesBloc.add( - ToggleOperationFileSelectionEvent(file), - ), - onDoubleTap: () => _handleDoubleClick(context, file), - child: Card( - margin: const EdgeInsets.only(bottom: 8), - elevation: 0, - // UX Fina: cambiamo colore del bordo se selezionato - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - side: BorderSide( - color: isSelected - ? Theme.of(context).colorScheme.primary - : Colors.grey.shade300, - width: isSelected ? 2 : 1, - ), - ), - // UX Fina: Sfondo leggermente colorato se selezionato - color: isSelected - ? Theme.of( - context, - ).colorScheme.primary.withValues(alpha: 0.05) - : Theme.of(context).colorScheme.surface, - child: ListTile( - leading: Icon( - isSelected - ? Icons.check_box - : Icons.check_box_outline_blank, - color: Theme.of(context).colorScheme.primary, - size: 32, - ), - title: Text( - file.name, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - subtitle: Text( - file.isLocal ? "$sizeMb MB • (Nuovo)" : " MB", - ), - trailing: Icon( - isPdf ? Icons.picture_as_pdf : Icons.image, - color: isPdf ? Colors.red : Colors.blue, - size: 32, - ), - ), - ), - ); - }, - ), - // --- PANNELLO AZIONI CONTESTUALI (LA MAGIA) --- - // Appare SOLO se c'è almeno un file selezionato - if (state.selectedFiles.isNotEmpty) - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - decoration: BoxDecoration( - color: Theme.of( - context, - ).colorScheme.primary.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: Theme.of( - context, - ).colorScheme.primary.withValues(alpha: 0.3), - ), - ), - child: Row( - children: [ - // Contatore - Text( - "${state.selectedFiles.length} file selezionati", - style: TextStyle( - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.primary, - ), - ), - const Spacer(), - - // Bottone Elimina - TextButton.icon( - style: TextButton.styleFrom( - foregroundColor: Colors.red, - ), - icon: const Icon(Icons.delete_outline), - label: const Text("Elimina"), - onPressed: () { - // Qui lancerai l'evento per eliminare i file selezionati! - // Es: operationFilesBloc.add(DeleteSelectedFilesEvent()); - }, - ), - const SizedBox(width: 8), - - // Bottone Copia - ElevatedButton.icon( - icon: const Icon(Icons.copy), - label: const Text("Copia in Cliente"), - onPressed: () { - final cubit = context.read(); - if (cubit.state.currentOperation?.customerId == - null) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context - .l10n - .operationFormAttachmentSectionNoCustomer, - ), - ), - ); - } else { - context.read().add( - LinkFilesToCustomerEvent( - customerId: cubit - .state - .currentOperation! - .customerId!, - ), - ); - } - }, - ), - ], - ), - ), - ), - ], - ], - ); - }, - ), - ); - } - - Future _handleGenerateQr(BuildContext context) async { - final cubit = context.read(); - var currentOperation = cubit.state.currentOperation; - - // 1. CATTURIAMO IL BLOC MENTRE SIAMO ANCORA NELLA PAGINA - final operationFilesBloc = context.read(); - - // 2. SE LA PRATICA E' NUOVA (Manca l'ID) - if (currentOperation == null || currentOperation.id == null) { - // NIENTE BlocListener qui! Solo un semplice Dialog di conferma - final bool? confirm = await showDialog( - context: context, - builder: (ctx) => AlertDialog( - title: const Text("Salvataggio Necessario"), - content: const Text( - "Per generare il QR Code e caricare file dal telefono, la pratica deve essere prima salvata in BOZZA.\n\nVuoi salvare ora?", - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(ctx, false), - child: const Text("Annulla"), - ), - ElevatedButton( - onPressed: () => Navigator.pop(ctx, true), - child: const Text("Salva in Bozza"), - ), - ], - ), - ); - - if (confirm != true) return; // Utente ha annullato - - // Salviamo forzatamente in bozza - await cubit.saveCurrentOperation( - targetStatus: OperationStatus.draft, - shouldPop: false, - ); - - // Recuperiamo il servizio aggiornato con l'ID! - currentOperation = cubit.state.currentOperation; - - if (currentOperation?.id == null) return; - } - - // 3. MOSTRIAMO IL QR CODE (Con il Ponte e l'Auto-Chiusura!) - if (context.mounted) { - final nomePratica = - "Pratica ${currentOperation?.customerDisplayName ?? ''}".trim(); - - showDialog( - context: context, - builder: (dialogContext) => BlocProvider.value( - // INIETTIAMO IL BLOC NEL CONTESTO DEL DIALOG ALIENO - value: operationFilesBloc, - - // ORA METTIAMO L'AUTO-CHIUSURA SUL QR CODE! - child: BlocListener( - listener: (context, state) { - // Se arrivano file remoti e lo stato è success, chiudiamo il QR! - // (Nota: usiamo dialogContext per assicurarci di chiudere il popup giusto) - if (state.status == OperationFilesStatus.success && - state.remoteFiles.isNotEmpty) { - Navigator.of(dialogContext).pop(); - } - }, - child: QrUploadDialog( - deepLinkUrl: - 'fluxapp:///operation/${currentOperation!.id}/upload?name=${Uri.encodeComponent(nomePratica)}', - title: 'Scatta per\n$nomePratica', - ), - ), - ), - ); - } - } - - // --- LOGICA DI VISUALIZZAZIONE OVERLAY --- - void _handleDoubleClick(BuildContext context, AttachmentModel file) { - showDialog( - context: context, - barrierDismissible: true, - builder: (ctx) => Dialog( - insetPadding: const EdgeInsets.all(16), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - child: ClipRRect( - borderRadius: BorderRadius.circular(12), - child: SizedBox( - width: double.infinity, - height: MediaQuery.of(context).size.height * 0.8, - child: file.isPdf - ? PdfViewerWidget( - storagePath: file.storagePath.isNotEmpty - ? file.storagePath - : null, - bytes: file.localBytes, - ) - : ImageViewerWidget( - storagePath: file.storagePath.isNotEmpty - ? file.storagePath - : null, - bytes: file.localBytes, - ), - ), - ), - ), - ); - } -} diff --git a/lib/features/operations/ui/operation_form_screen/customer_section.dart b/lib/features/operations/ui/operation_form_screen/customer_section.dart deleted file mode 100644 index b3546db..0000000 --- a/lib/features/operations/ui/operation_form_screen/customer_section.dart +++ /dev/null @@ -1,96 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flux/features/customers/ui/customer_search_sheet.dart'; -import 'package:flux/features/operations/models/operation_model.dart'; - -class CustomerSection extends StatelessWidget { - final OperationModel operation; - - const CustomerSection({super.key, required this.operation}); - - void _openCustomerSearch(BuildContext context) { - showModalBottomSheet( - context: context, - isScrollControlled: true, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(20)), - ), - builder: (modalContext) { - return Padding( - padding: EdgeInsets.only( - bottom: MediaQuery.of(modalContext).viewInsets.bottom, - ), - // La modale di ricerca - child: const CustomerSearchSheet(), - ); - }, - ); - } - - @override - Widget build(BuildContext context) { - // Niente BlocBuilder qui! Leggiamo solo la variabile 'operation' - final hasCustomer = operation.customerId != null; - - return Card( - elevation: 2, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - Icons.person, - color: Theme.of(context).colorScheme.primary, - ), - const SizedBox(width: 8), - Text( - "Dati Cliente", - style: Theme.of(context).textTheme.titleMedium, - ), - ], - ), - const SizedBox(height: 16), - - if (!hasCustomer) - Center( - child: ElevatedButton.icon( - onPressed: () => _openCustomerSearch(context), - icon: const Icon(Icons.search), - label: const Text("Seleziona o Crea Cliente"), - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric( - horizontal: 24, - vertical: 12, - ), - ), - ), - ) - else - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - operation.customerDisplayName ?? "Cliente Selezionato", - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - ), - TextButton.icon( - onPressed: () => _openCustomerSearch(context), - icon: const Icon(Icons.edit, size: 18), - label: const Text("Cambia"), - ), - ], - ), - ], - ), - ), - ); - } -} diff --git a/lib/features/operations/ui/operation_form_screen/energy_operation_dialog.dart b/lib/features/operations/ui/operation_form_screen/energy_operation_dialog.dart deleted file mode 100644 index f44dd40..0000000 --- a/lib/features/operations/ui/operation_form_screen/energy_operation_dialog.dart +++ /dev/null @@ -1,417 +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/operations/models/energy_operation_model.dart'; // Assicurati degli import - -class EnergyOperationDialog extends StatefulWidget { - final List initialOperations; - final String - currentStoreId; // Ci serve per sapere per quale negozio caricare i gestori - - const EnergyOperationDialog({ - super.key, - required this.initialOperations, - required this.currentStoreId, - }); - - @override - State createState() => _EnergyOperationDialogState(); -} - -class _EnergyOperationDialogState extends State { - // Lista temporanea per non "sporcare" il cubit finché non si preme Conferma - late List _tempList; - bool _isAddingNew = false; - - @override - void initState() { - super.initState(); - _tempList = List.from(widget.initialOperations); - // Al caricamento della modale, chiediamo al Cubit di recuperare i gestori veri! - context.read().loadActiveProvidersForStore( - widget.currentStoreId, - ); - } - - @override - Widget build(BuildContext context) { - return AlertDialog( - title: Row( - children: [ - Icon(Icons.bolt, color: Theme.of(context).colorScheme.primary), - const SizedBox(width: 8), - Text(_isAddingNew ? "Nuovo Contratto" : "Servizi Energia"), - ], - ), - content: AnimatedSize( - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - child: SizedBox( - width: double.maxFinite, - // Cambia vista in base al flag - child: _isAddingNew - ? _EnergyForm( - onSave: (newOperation) { - setState(() { - _tempList.add(newOperation); - _isAddingNew = false; // Torna alla lista - }); - }, - onCancel: () { - setState(() => _isAddingNew = false); - }, - ) - : _EnergyList( - operations: _tempList, - onDelete: (index) { - setState(() => _tempList.removeAt(index)); - }, - onAddTap: () { - setState(() => _isAddingNew = true); // Passa al form - }, - activeProviders: [ - // Passiamo i provider attivi filtrati per tipo Energia - ...context - .read() - .state - .activeProviders - .where((p) => p.energia == true), - ], - ), - ), - ), - actions: [ - if (!_isAddingNew) ...[ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text("Annulla"), - ), - ElevatedButton( - onPressed: () => Navigator.pop(context, _tempList), - child: const Text("Conferma Tutti"), - ), - ], - ], - ); - } -} - -// ========================================== -// VISTA 1: LA LISTA DEI CONTRATTI -// ========================================== -class _EnergyList extends StatelessWidget { - final List operations; - final List - activeProviders; // <--- NUOVO: La lista vera dal Cubit - final Function(int) onDelete; - final VoidCallback onAddTap; - - const _EnergyList({ - required this.operations, - required this.activeProviders, // <--- Richiesto - required this.onDelete, - required this.onAddTap, - }); - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (operations.isEmpty) - const Padding( - padding: EdgeInsets.symmetric(vertical: 32.0), - child: Text( - "Nessun contratto energia inserito.", - textAlign: TextAlign.center, - style: TextStyle(color: Colors.grey), - ), - ) - else - Flexible( - child: ListView.separated( - shrinkWrap: true, - itemCount: operations.length, - separatorBuilder: (_, _) => const Divider(height: 1), - itemBuilder: (context, index) { - final s = operations[index]; - final isLuce = s.type == EnergyType.luce; - - // LA MAGIA: Troviamo il nome partendo dall'ID salvato nel servizio - final providerIndex = activeProviders.indexWhere( - (p) => p.id == s.providerId, - ); - final providerName = providerIndex >= 0 - ? (activeProviders[providerIndex].nome) - : 'Gestore Rimosso/Sconosciuto'; - - // Formattazione data pulita (es. 04/09/2025) - final day = s.expiration.day.toString().padLeft(2, '0'); - final month = s.expiration.month.toString().padLeft(2, '0'); - final formattedDate = "$day/$month/${s.expiration.year}"; - - return ListTile( - contentPadding: EdgeInsets.zero, - leading: CircleAvatar( - backgroundColor: isLuce - ? Colors.orange.shade100 - : Colors.blue.shade100, - child: Icon( - isLuce - ? Icons.lightbulb_outline - : Icons.local_fire_department, - color: isLuce ? Colors.orange : Colors.blue, - ), - ), - title: Text( - providerName, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - subtitle: Text("Scadenza: $formattedDate"), - trailing: IconButton( - icon: const Icon(Icons.delete_outline, color: Colors.red), - onPressed: () => onDelete(index), - ), - ); - }, - ), - ), - const SizedBox(height: 16), - OutlinedButton.icon( - onPressed: onAddTap, - icon: const Icon(Icons.add), - label: const Text("Aggiungi Contratto"), - ), - ], - ); - } -} - -// ========================================== -// VISTA 2: IL FORM DI INSERIMENTO -// ========================================== -class _EnergyForm extends StatefulWidget { - final Function(EnergyOperationModel) onSave; - final VoidCallback onCancel; - - const _EnergyForm({required this.onSave, required this.onCancel}); - - @override - State<_EnergyForm> createState() => _EnergyFormState(); -} - -class _EnergyFormState extends State<_EnergyForm> { - EnergyType _selectedType = EnergyType.luce; - String? _selectedProviderId; - DateTime? _selectedExpiration; - int? _selectedMonthsPreset; - - void _applyPreset(int? months) { - if (months == null) return; - setState(() { - _selectedMonthsPreset = months; - // Calcoliamo la data: oggi + X mesi - final now = DateTime.now(); - _selectedExpiration = DateTime(now.year, now.month + months, now.day); - }); - } - - Future _pickDate() async { - final picked = await showDatePicker( - context: context, - initialDate: DateTime.now().add( - const Duration(days: 365), - ), // Default 1 anno - firstDate: DateTime.now(), - lastDate: DateTime.now().add(const Duration(days: 365 * 10)), - ); - if (picked != null) { - setState(() => _selectedExpiration = picked); - } - } - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // 1. Tipo (Luce o Gas) - Segmented Button stile M3 - SegmentedButton( - segments: const [ - ButtonSegment( - value: EnergyType.luce, - label: Text("Luce"), - icon: Icon(Icons.lightbulb_outline), - ), - ButtonSegment( - value: EnergyType.gas, - label: Text("Gas"), - icon: Icon(Icons.local_fire_department), - ), - ], - selected: {_selectedType}, - onSelectionChanged: (Set newSelection) { - setState(() => _selectedType = newSelection.first); - }, - ), - const SizedBox(height: 20), - // 2. SCADENZA INTELLIGENTE (La parte PRO) - const Text( - "Scadenza Contratto", - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 12, - color: Colors.grey, - ), - ), - const SizedBox(height: 8), - - SegmentedButton( - showSelectedIcon: false, // Per un look più pulito - segments: const [ - ButtonSegment(value: 12, label: Text("12m")), - ButtonSegment(value: 24, label: Text("24m")), - ButtonSegment(value: 36, label: Text("36m")), - ButtonSegment( - value: null, - label: Icon(Icons.calendar_month, size: 20), - ), - ], - selected: {_selectedMonthsPreset}, - onSelectionChanged: (Set newSelection) { - final val = newSelection.first; - if (val == null) { - _pickDate(); // Se clicca l'icona calendario, apre il picker - } else { - _applyPreset(val); // Altrimenti applica 12, 24 o 36 - } - }, - ), - - const SizedBox(height: 12), - - // Visualizzazione della data calcolata (o scelta) - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Theme.of( - context, - ).colorScheme.surfaceContainerHighest.withValues(alpha: 0.5), - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: _selectedExpiration != null - ? Theme.of(context).colorScheme.primary - : Colors.transparent, - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.event, - size: 18, - color: _selectedExpiration != null - ? Theme.of(context).colorScheme.primary - : Colors.grey, - ), - const SizedBox(width: 8), - Text( - _selectedExpiration != null - ? "Scade il: ${_selectedExpiration!.day.toString().padLeft(2, '0')}/${_selectedExpiration!.month.toString().padLeft(2, '0')}/${_selectedExpiration!.year}" - : "Seleziona una scadenza", - style: TextStyle( - fontWeight: FontWeight.bold, - color: _selectedExpiration != null - ? Theme.of(context).colorScheme.onSurface - : Colors.grey, - ), - ), - ], - ), - ), - - const SizedBox(height: 20), - - // 2. Provider Dropdown - BlocBuilder( - builder: (context, state) { - if (state.isLoading) { - return const Center( - child: LinearProgressIndicator(), - ); // Mostra una barretta di caricamento - } - - if (state.activeProviders.isEmpty) { - return const Text( - "Nessun gestore associato a questo negozio.", - style: TextStyle(color: Colors.red), - ); - } - // Filtra solo i provider di tipo Energia (Se hai una categoria nel modello) - // Se non hai una categoria nel ProviderModel, puoi rimuovere il .where - final energyProviders = state.activeProviders; - return DropdownButtonFormField( - decoration: const InputDecoration( - labelText: "Gestore / Provider", - border: OutlineInputBorder(), - ), - initialValue: _selectedProviderId, - items: energyProviders.map((p) { - return DropdownMenuItem(value: p.id, child: Text(p.nome)); - }).toList(), - onChanged: (val) => setState(() => _selectedProviderId = val), - ); - }, - ), - const SizedBox(height: 16), - - // 3. Scadenza (DatePicker integrato in un TextField) - TextFormField( - readOnly: true, - onTap: _pickDate, - decoration: InputDecoration( - labelText: "Data Scadenza", - border: const OutlineInputBorder(), - suffixIcon: const Icon(Icons.calendar_month), - ), - // Mostra la data se selezionata, altrimenti vuoto - controller: TextEditingController( - text: _selectedExpiration != null - ? "${_selectedExpiration!.day}/${_selectedExpiration!.month}/${_selectedExpiration!.year}" - : "", - ), - ), - const SizedBox(height: 24), - - // 4. Pulsanti Interni al Form - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: widget.onCancel, - child: const Text("Indietro"), - ), - const SizedBox(width: 8), - ElevatedButton( - onPressed: - (_selectedProviderId == null || _selectedExpiration == null) - ? null // Disabilitato se mancano dati obbligatori - : () { - final newOperation = EnergyOperationModel( - type: _selectedType, - expiration: _selectedExpiration!, - providerId: _selectedProviderId!, - ); - widget.onSave(newOperation); - }, - child: const Text("Salva Contratto"), - ), - ], - ), - ], - ); - } -} diff --git a/lib/features/operations/ui/operation_form_screen/entertainment_operation_card.dart b/lib/features/operations/ui/operation_form_screen/entertainment_operation_card.dart deleted file mode 100644 index 88fbc27..0000000 --- a/lib/features/operations/ui/operation_form_screen/entertainment_operation_card.dart +++ /dev/null @@ -1,393 +0,0 @@ -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_cubit.dart'; -import 'package:flux/features/master_data/providers/models/provider_model.dart'; -import 'package:flux/features/operations/data/operations_repository.dart'; -import 'package:flux/features/operations/models/entertainment_operation_model.dart'; -import 'package:get_it/get_it.dart'; - -class EntertainmentOperationDialog extends StatefulWidget { - final List initialOperations; - final String currentStoreId; - - const EntertainmentOperationDialog({ - super.key, - required this.initialOperations, - required this.currentStoreId, - }); - - @override - State createState() => - _EntertainmentOperationDialogState(); -} - -class _EntertainmentOperationDialogState - extends State { - late List _tempList; - bool _isAddingNew = false; - - @override - void initState() { - super.initState(); - _tempList = List.from(widget.initialOperations); - // Carichiamo i provider attivi per lo store corrente - context.read().loadActiveProvidersForStore( - widget.currentStoreId, - ); - } - - @override - Widget build(BuildContext context) { - return AlertDialog( - title: Row( - children: [ - Icon( - Icons.movie_filter_outlined, - color: Theme.of(context).colorScheme.primary, - ), - const SizedBox(width: 8), - Text(_isAddingNew ? "Nuovo Servizio" : "Servizi Intrattenimento"), - ], - ), - content: AnimatedSize( - duration: const Duration(milliseconds: 300), - child: SizedBox( - width: MediaQuery.of(context).size.width * 0.9, - child: _isAddingNew - ? _EntertainmentForm( - // Il form che abbiamo creato prima - onSave: (newOperation) => setState(() { - _tempList.add(newOperation); - _isAddingNew = false; - }), - onCancel: () => setState(() => _isAddingNew = false), - ) - : BlocBuilder( - builder: (context, state) { - // Passiamo allProviders per garantire la visione dello storico - return _EntertainmentList( - operations: _tempList, - allProviders: state.allProviders, - onDelete: (index) => - setState(() => _tempList.removeAt(index)), - onAddTap: () => setState(() => _isAddingNew = true), - ); - }, - ), - ), - ), - actions: !_isAddingNew - ? [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text("Annulla"), - ), - ElevatedButton( - onPressed: () => Navigator.pop(context, _tempList), - child: const Text("Conferma Tutti"), - ), - ] - : null, // I pulsanti del form sono interni al form stesso - ); - } -} - -class _EntertainmentList extends StatelessWidget { - final List operations; - final List allProviders; - final Function(int) onDelete; - final VoidCallback onAddTap; - - const _EntertainmentList({ - required this.operations, - required this.allProviders, - required this.onDelete, - required this.onAddTap, - }); - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (operations.isEmpty) - const Padding( - padding: EdgeInsets.symmetric(vertical: 32.0), - child: Text( - "Nessun servizio intrattenimento.", - textAlign: TextAlign.center, - style: TextStyle(color: Colors.grey), - ), - ) - else - Flexible( - child: ListView.separated( - shrinkWrap: true, - itemCount: operations.length, - separatorBuilder: (_, _) => const Divider(height: 1), - itemBuilder: (context, index) { - final s = operations[index]; - - final providerName = allProviders - .firstWhere( - (p) => p.id == s.providerId, - orElse: () => ProviderModel( - id: '', - nome: 'Fornitore Storico', - companyId: '', - isActive: false, - energia: false, - telefoniaFissa: false, - telefoniaMobile: false, - assicurazioni: false, - finanziamenti: false, - altro: false, - intrattenimento: false, - ), - ) - .nome; - - return ListTile( - contentPadding: EdgeInsets.zero, - leading: CircleAvatar( - backgroundColor: Colors.purple.shade100, - child: const Icon( - Icons.movie_creation_outlined, - color: Colors.purple, - ), - ), - title: Text( - "${s.type} • $providerName", - style: const TextStyle(fontWeight: FontWeight.bold), - ), - subtitle: Text( - s.constrained - ? "Vincolo fino al: ${s.constrainExpiration.day}/${s.constrainExpiration.month}/${s.constrainExpiration.year}" - : "Senza vincoli", - style: TextStyle( - color: s.constrained - ? Colors.red.shade700 - : Colors.green.shade700, - ), - ), - trailing: IconButton( - icon: const Icon(Icons.delete_outline, color: Colors.red), - onPressed: () => onDelete(index), - ), - ); - }, - ), - ), - const SizedBox(height: 16), - OutlinedButton.icon( - onPressed: onAddTap, - icon: const Icon(Icons.add), - label: const Text("Aggiungi Servizio"), - ), - ], - ); - } -} - -// ---ENTERTAINMENT FORM (MODALE)--- - -class _EntertainmentForm extends StatefulWidget { - final Function(EntertainmentOperationModel) onSave; - final VoidCallback onCancel; - - const _EntertainmentForm({required this.onSave, required this.onCancel}); - - @override - State<_EntertainmentForm> createState() => _EntertainmentFormState(); -} - -class _EntertainmentFormState extends State<_EntertainmentForm> { - String? _selectedProviderId; - final TextEditingController _typeController = TextEditingController(); - bool _isConstrained = false; - DateTime _expirationDate = DateTime.now().add( - const Duration(days: 365), - ); // Default 12 mesi - - // Preset rapidi per il vincolo (es: 12, 24 mesi) - int? _selectedPresetMonths; - - void _applyPreset(int months) { - setState(() { - _selectedPresetMonths = months; - _isConstrained = true; - final now = DateTime.now(); - _expirationDate = DateTime(now.year, now.month + months, now.day); - }); - } - - Future _pickDate() async { - final picked = await showDatePicker( - context: context, - initialDate: _expirationDate, - firstDate: DateTime.now(), - lastDate: DateTime.now().add(const Duration(days: 365 * 10)), - ); - if (picked != null) { - setState(() { - _expirationDate = picked; - _selectedPresetMonths = null; - _isConstrained = true; - }); - } - } - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // 1. GESTORE (Filtro intrattenimento) - BlocBuilder( - builder: (context, state) { - final filtered = state.activeProviders - .where((p) => p.intrattenimento) - .toList(); - return DropdownButtonFormField( - decoration: const InputDecoration( - labelText: "Fornitore (es: Sky, TIM)", - border: OutlineInputBorder(), - ), - items: filtered - .map( - (p) => DropdownMenuItem(value: p.id, child: Text(p.nome)), - ) - .toList(), - onChanged: (val) => setState(() => _selectedProviderId = val), - ); - }, - ), - const SizedBox(height: 16), - - // 2. TIPO SERVIZIO (TextField con suggerimenti rapidi sotto) - TextFormField( - controller: _typeController, - decoration: const InputDecoration( - labelText: "Servizio", - hintText: "es: Netflix, DAZN, Disney+", - border: OutlineInputBorder(), - ), - onChanged: (val) => setState(() {}), - ), - const SizedBox(height: 8), - // Suggerimenti rapidi (Chip) - FutureBuilder>( - future: GetIt.I().fetchTopEntertainmentTypes( - GetIt.I().state.company!.id!, - ), - builder: (context, snapshot) { - final suggestions = snapshot.data ?? ["Netflix", "DAZN", "Sky"]; - return Wrap( - spacing: 8, - children: suggestions.map((s) { - return ActionChip( - label: Text(s, style: const TextStyle(fontSize: 12)), - onPressed: () => setState(() => _typeController.text = s), - ); - }).toList(), - ); - }, - ), - const SizedBox(height: 16), - - // 3. VINCOLO CONTRATTUALE - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - "Vincolo di permanenza", - style: TextStyle(fontWeight: FontWeight.bold), - ), - Switch( - value: _isConstrained, - onChanged: (val) => setState(() { - _isConstrained = val; - if (!val) _selectedPresetMonths = null; - }), - ), - ], - ), - - if (_isConstrained) ...[ - const SizedBox(height: 8), - SegmentedButton( - segments: const [ - ButtonSegment(value: 12, label: Text("12m")), - ButtonSegment(value: 24, label: Text("24m")), - ButtonSegment( - value: null, - label: Icon(Icons.calendar_month, size: 20), - ), - ], - selected: {_selectedPresetMonths}, - onSelectionChanged: (val) { - if (val.first == null) { - _pickDate(); - } else { - _applyPreset(val.first!); - } - }, - ), - const SizedBox(height: 12), - // Box data scadenza vincolo - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Theme.of( - context, - ).colorScheme.surfaceContainerHighest.withValues(alpha: 0.5), - borderRadius: BorderRadius.circular(8), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.event_busy, size: 18, color: Colors.redAccent), - const SizedBox(width: 8), - Text( - "Scadenza vincolo: ${_expirationDate.day.toString().padLeft(2, '0')}/${_expirationDate.month.toString().padLeft(2, '0')}/${_expirationDate.year}", - style: const TextStyle(fontWeight: FontWeight.bold), - ), - ], - ), - ), - ], - - const SizedBox(height: 24), - - // PULSANTI - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: widget.onCancel, - child: const Text("Annulla"), - ), - const SizedBox(width: 8), - ElevatedButton( - onPressed: - (_selectedProviderId == null || _typeController.text.isEmpty) - ? null - : () => widget.onSave( - EntertainmentOperationModel( - providerId: _selectedProviderId!, - type: _typeController.text, - constrained: _isConstrained, - constrainExpiration: _expirationDate, - ), - ), - child: const Text("Aggiungi"), - ), - ], - ), - ], - ); - } -} diff --git a/lib/features/operations/ui/operation_form_screen/finance_operation_dialog.dart b/lib/features/operations/ui/operation_form_screen/finance_operation_dialog.dart deleted file mode 100644 index fa838a9..0000000 --- a/lib/features/operations/ui/operation_form_screen/finance_operation_dialog.dart +++ /dev/null @@ -1,479 +0,0 @@ -import 'dart:async'; - -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/models/model_model.dart'; -import 'package:flux/features/master_data/providers/blocs/provider_cubit.dart'; -import 'package:flux/features/operations/models/fin_operation_model.dart'; -import 'package:flux/features/master_data/providers/models/provider_model.dart'; - -// =========================================================================== -// DIALOG PRINCIPALE -// =========================================================================== -class FinanceOperationDialog extends StatefulWidget { - final List initialOperations; - final String currentStoreId; - final ProductCubit productCubit; - - const FinanceOperationDialog({ - super.key, - required this.initialOperations, - required this.currentStoreId, - required this.productCubit, - }); - - @override - State createState() => _FinanceOperationDialogState(); -} - -class _FinanceOperationDialogState extends State { - late List _tempList; - bool _isAddingNew = false; - - @override - void initState() { - super.initState(); - _tempList = List.from(widget.initialOperations); - // Carichiamo i dati necessari dai Cubit - context.read().loadActiveProvidersForStore( - widget.currentStoreId, - ); - context.read().loadBrands(); - } - - @override - Widget build(BuildContext context) { - return BlocProvider.value( - value: widget.productCubit, - child: AlertDialog( - title: Row( - children: [ - Icon( - Icons.payments_outlined, - color: Theme.of(context).colorScheme.primary, - ), - const SizedBox(width: 8), - Text(_isAddingNew ? "Dettagli Finanziamento" : "Finanziamenti"), - ], - ), - content: AnimatedSize( - duration: const Duration(milliseconds: 300), - child: SizedBox( - width: MediaQuery.of(context).size.width * 0.9, - child: _isAddingNew - ? _FinanceForm( - onSave: (newFin) => setState(() { - _tempList.add(newFin); - _isAddingNew = false; - }), - onCancel: () => setState(() => _isAddingNew = false), - ) - : BlocBuilder( - builder: (context, provState) { - return BlocBuilder( - builder: (context, prodState) { - return _FinanceList( - operations: _tempList, - allProviders: - provState.allProviders, // Per vedere lo storico - allModels: prodState.models, - onDelete: (index) => - setState(() => _tempList.removeAt(index)), - onAddTap: () => setState(() => _isAddingNew = true), - ); - }, - ); - }, - ), - ), - ), - actions: !_isAddingNew - ? [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text("Annulla"), - ), - ElevatedButton( - onPressed: () => Navigator.pop(context, _tempList), - child: const Text("Conferma"), - ), - ] - : null, - ), - ); - } -} - -// =========================================================================== -// VISTA LISTA (STORICA) -// =========================================================================== -class _FinanceList extends StatelessWidget { - final List operations; - final List allProviders; - final List allModels; - final Function(int) onDelete; - final VoidCallback onAddTap; - - const _FinanceList({ - required this.operations, - required this.allProviders, - required this.allModels, - required this.onDelete, - required this.onAddTap, - }); - - @override - Widget build(BuildContext context) { - if (operations.isEmpty) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Padding( - padding: EdgeInsets.symmetric(vertical: 32.0), - child: Text( - "Nessun finanziamento inserito.", - style: TextStyle(color: Colors.grey), - ), - ), - OutlinedButton.icon( - onPressed: onAddTap, - icon: const Icon(Icons.add), - label: const Text("Aggiungi primo"), - ), - ], - ); - } - - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: ListView.separated( - shrinkWrap: true, - itemCount: operations.length, - separatorBuilder: (_, _) => const Divider(), - itemBuilder: (context, index) { - final s = operations[index]; - - // Cerchiamo il nome del provider in TUTTI quelli caricati (storico) - final providerName = allProviders - .firstWhere( - (p) => p.id == s.providerId, - orElse: () => ProviderModel( - id: '', - nome: 'Operatore Storico', - companyId: '', - isActive: false, - energia: false, - telefoniaFissa: false, - telefoniaMobile: false, - assicurazioni: false, - altro: false, - intrattenimento: false, - finanziamenti: false, - ), - ) - .nome; - - // Cerchiamo il nome del modello - final modelName = allModels - .firstWhere( - (m) => m.id == s.modelId, - orElse: () => ModelModel( - id: '', - name: 'Prodotto', - nameWithBrand: 'Prodotto Storico', - brandId: '', - ), - ) - .nameWithBrand; - - final dateStr = - "${s.expiration.day.toString().padLeft(2, '0')}/${s.expiration.month.toString().padLeft(2, '0')}/${s.expiration.year}"; - - return ListTile( - title: Text( - modelName, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - subtitle: Text("$providerName • Scade: $dateStr"), - trailing: IconButton( - icon: const Icon(Icons.delete_outline, color: Colors.red), - onPressed: () => onDelete(index), - ), - ); - }, - ), - ), - const SizedBox(height: 16), - TextButton.icon( - onPressed: onAddTap, - icon: const Icon(Icons.add), - label: const Text("Aggiungi altro"), - ), - ], - ); - } -} - -// =========================================================================== -// FORM CON OMNI-SEARCH -// =========================================================================== -class _FinanceForm extends StatefulWidget { - final Function(FinOperationModel) onSave; - final VoidCallback onCancel; - - const _FinanceForm({required this.onSave, required this.onCancel}); - - @override - State<_FinanceForm> createState() => _FinanceFormState(); -} - -class _FinanceFormState extends State<_FinanceForm> { - String? _selectedProviderId; - ModelModel? _selectedModel; - int _selectedMonths = 30; // Default richiesto - Timer? _debounce; - final TextEditingController _searchController = TextEditingController(); - late DateTime _selectedExpirationDate; - - @override - void initState() { - super.initState(); - final now = DateTime.now(); - _selectedExpirationDate = DateTime( - now.year, - now.month + _selectedMonths, - now.day, - ); // Inizialmente 30 mesi dalla data attuale - } - - void _onSearchChanged(String query) { - if (_debounce?.isActive ?? false) _debounce!.cancel(); - _debounce = Timer(const Duration(milliseconds: 500), () { - context.read().searchModels(query); - }); - } - - // Funzione per aggiornare la data quando si clicca sui segmenti 24, 30, 48 - void _updateExpirationByMonths(int months) { - setState(() { - _selectedMonths = months; - final now = DateTime.now(); - // Calcolo preciso: aggiungiamo i mesi alla data attuale - _selectedExpirationDate = DateTime(now.year, now.month + months, now.day); - }); - } - - // Funzione per il picker manuale - Future _selectManualDate() async { - final DateTime? picked = await showDatePicker( - context: context, - initialDate: _selectedExpirationDate, - firstDate: DateTime.now(), - lastDate: DateTime.now().add( - const Duration(days: 365 * 10), - ), // Fino a 10 anni - ); - if (picked != null && picked != _selectedExpirationDate) { - setState(() { - _selectedExpirationDate = picked; - _selectedMonths = 0; // Resettiamo i segmenti perché è una data custom - }); - } - } - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // 1. SCELTA ISTITUTO (Solo attivi) - BlocBuilder( - builder: (context, state) { - final finProviders = state.activeProviders - .where((p) => p.finanziamenti) - .toList(); // Già filtrati dal caricamento della dialog - return DropdownButtonFormField( - initialValue: _selectedProviderId, - decoration: const InputDecoration( - labelText: "Gestore", - border: OutlineInputBorder(), - ), - items: finProviders - .map( - (p) => DropdownMenuItem(value: p.id, child: Text(p.nome)), - ) - .toList(), - onChanged: (val) => setState(() => _selectedProviderId = val), - ); - }, - ), - const SizedBox(height: 16), - - // 2. RICERCA MODELLO - if (_selectedModel == null) ...[ - TextField( - controller: _searchController, - decoration: InputDecoration( - hintText: "Cerca modello (es: iPhone...)", - prefixIcon: const Icon(Icons.search), - border: const OutlineInputBorder(), - suffixIcon: IconButton( - icon: const Icon(Icons.add_circle_outline), - onPressed: () => _showQuickCreate(context), - ), - ), - onChanged: (val) { - _onSearchChanged(val); - }, - ), - const SizedBox(height: 8), - _buildSearchSuggestions(), - ] else - Card( - color: Theme.of(context).colorScheme.secondaryContainer, - child: ListTile( - leading: const Icon(Icons.phone_android), - title: Text( - _selectedModel!.nameWithBrand, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - trailing: IconButton( - icon: const Icon(Icons.close), - onPressed: () => setState(() => _selectedModel = null), - ), - ), - ), - - const SizedBox(height: 16), - - // 3. DURATA PRESET - const Text( - "Durata Rate", - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 12, - color: Colors.grey, - ), - ), - const SizedBox(height: 8), - SegmentedButton( - segments: const [ - ButtonSegment(value: 24, label: Text("24m")), - ButtonSegment(value: 30, label: Text("30m")), - ButtonSegment(value: 48, label: Text("48m")), - ], - selected: {_selectedMonths}, - onSelectionChanged: (val) => _updateExpirationByMonths(val.first), - ), - - const SizedBox(height: 16), - - // RIEPILOGO DATA E PICKER MANUALE (Stile Energia) - const Text( - "Scadenza Finanziamento", - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 12, - color: Colors.grey, - ), - ), - const SizedBox(height: 4), - InkWell( - onTap: _selectManualDate, - child: Container( - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12), - decoration: BoxDecoration( - border: Border.all(color: Colors.grey.shade400), - borderRadius: BorderRadius.circular(8), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - const Icon( - Icons.calendar_today, - size: 18, - color: Colors.blue, - ), - const SizedBox(width: 12), - Text( - "${_selectedExpirationDate.day.toString().padLeft(2, '0')}/${_selectedExpirationDate.month.toString().padLeft(2, '0')}/${_selectedExpirationDate.year}", - style: const TextStyle(fontSize: 16), - ), - ], - ), - const Icon(Icons.edit, size: 18, color: Colors.grey), - ], - ), - ), - ), - const SizedBox(height: 24), - - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: widget.onCancel, - child: const Text("Indietro"), - ), - const SizedBox(width: 8), - ElevatedButton( - onPressed: (_selectedProviderId == null || _selectedModel == null) - ? null - : () { - final now = DateTime.now(); - widget.onSave( - FinOperationModel( - providerId: _selectedProviderId!, - modelId: _selectedModel!.id!, - expiration: DateTime( - now.year, - now.month + _selectedMonths, - now.day, - ), - ), - ); - }, - child: const Text("Salva"), - ), - ], - ), - ], - ); - } - - Widget _buildSearchSuggestions() { - return BlocBuilder( - builder: (context, state) { - final query = _searchController.text.toLowerCase(); - if (query.isEmpty) return const SizedBox.shrink(); - - final filtered = state.models - .where((m) => m.nameWithBrand.toLowerCase().contains(query)) - .take(3) - .toList(); - - return Column( - children: filtered - .map( - (m) => ListTile( - title: Text(m.nameWithBrand), - onTap: () => setState(() => _selectedModel = m), - dense: true, - ), - ) - .toList(), - ); - }, - ); - } - - void _showQuickCreate(BuildContext context) { - // Implementazione rapida dialog creazione Brand/Modello come discusso prima - } -} diff --git a/lib/features/operations/ui/operation_form_screen/general_info_section.dart b/lib/features/operations/ui/operation_form_screen/general_info_section.dart deleted file mode 100644 index 183224c..0000000 --- a/lib/features/operations/ui/operation_form_screen/general_info_section.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flux/features/operations/models/operation_model.dart'; - -class GeneralInfoSection extends StatelessWidget { - final OperationModel operation; - const GeneralInfoSection({super.key, required this.operation}); - - @override - Widget build(BuildContext context) { - return Card( - elevation: 2, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - Icons.info_outline, - color: Theme.of(context).colorScheme.primary, - ), - const SizedBox(width: 8), - Text( - "Info Generali", - style: Theme.of(context).textTheme.titleMedium, - ), - ], - ), - const SizedBox(height: 16), - - // Numero di Riferimento / Telefono - TextFormField( - initialValue: operation.reference, - keyboardType: TextInputType - .phone, // Fa aprire il tastierino numerico su mobile - decoration: const InputDecoration( - labelText: "Numero di Telefono / Riferimento", - hintText: "Es. 3331234567", - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.phone), - ), - ), - const SizedBox(height: 16), - - // Campo Note - TextFormField( - initialValue: operation.note, - maxLines: 4, - minLines: 2, - decoration: const InputDecoration( - labelText: "Note Operazione", - hintText: - "Scrivi qui eventuali dettagli o richieste del cliente...", - border: OutlineInputBorder(), - alignLabelWithHint: true, - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/features/operations/ui/operation_form_screen/int_dialogs.dart b/lib/features/operations/ui/operation_form_screen/int_dialogs.dart deleted file mode 100644 index cbda2e7..0000000 --- a/lib/features/operations/ui/operation_form_screen/int_dialogs.dart +++ /dev/null @@ -1,158 +0,0 @@ -import 'dart:async'; // Necessario per il Timer -import 'package:flutter/material.dart'; - -Future updateCountDialog( - BuildContext context, - String title, - int currentValue, - Function(int) onSave, -) async { - int tempValue = - currentValue; // Variabile locale per gestire il conteggio nella dialog - - final result = await showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text("Imposta $title"), - content: QuickCounter( - initialValue: tempValue, - onChanged: (val) => tempValue = - val, // Aggiorna il valore locale quando il counter cambia - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text("Annulla"), - ), - ElevatedButton( - onPressed: () => Navigator.pop(context, tempValue), - child: const Text("Conferma"), - ), - ], - ), - ); - - if (result != null) { - onSave(result); - } -} - -// --- Widget Interno Specifico per il Counter Veloce --- -class QuickCounter extends StatefulWidget { - final int initialValue; - final ValueChanged - onChanged; // Callback per notificare il padre dei cambiamenti - - const QuickCounter({ - super.key, - required this.initialValue, - required this.onChanged, - }); - - @override - State createState() => _QuickCounterState(); -} - -class _QuickCounterState extends State { - late int _value; - Timer? _longPressTimer; // Il timer per l'auto-incremento - - @override - void initState() { - super.initState(); - _value = widget.initialValue; - } - - @override - void dispose() { - _longPressTimer - ?.cancel(); // IMPORTANTE: Annulla sempre il timer alla distruzione - super.dispose(); - } - - // Logica comune per incremento/decremento singolo o rapido - void _update(int delta) { - setState(() { - _value += delta; - if (_value < 0) _value = 0; // Impedisci numeri negativi - }); - widget.onChanged(_value); // Notifica il padre - } - - // Gestione dell'inizio della pressione prolungata - void _startLongPress(int delta) { - _update(delta); // Esegui subito il primo aggiornamento al tocco iniziale - _longPressTimer = Timer.periodic(const Duration(milliseconds: 100), ( - timer, - ) { - _update(delta); // Aggiorna velocemente finché la pressione continua - }); - } - - // Gestione della fine della pressione prolungata - void _stopLongPress() { - _longPressTimer?.cancel(); - } - - @override - Widget build(BuildContext context) { - final canDecrement = _value > 0; - - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // --- Pulsante MENO --- - GestureDetector( - onLongPressStart: canDecrement ? (_) => _startLongPress(-1) : null, - onLongPressEnd: (_) => _stopLongPress(), - onLongPressCancel: () => _stopLongPress(), - onTap: canDecrement ? () => _update(-1) : null, - child: Opacity( - // Visivamente disabilitato se < 0 - opacity: canDecrement ? 1.0 : 0.4, - child: const ActionButton(icon: Icons.remove, color: Colors.red), - ), - ), - - // --- Valore Centrale --- - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: Text( - _value.toString(), - style: const TextStyle(fontSize: 40, fontWeight: FontWeight.bold), - ), - ), - - // --- Pulsante PIU' --- - GestureDetector( - onLongPressStart: (_) => _startLongPress(1), - onLongPressEnd: (_) => _stopLongPress(), - onLongPressCancel: () => _stopLongPress(), - onTap: () => _update(1), - child: const ActionButton(icon: Icons.add, color: Colors.green), - ), - ], - ); - } -} - -// Piccolo widget di utilità per l'aspetto del pulsante -class ActionButton extends StatelessWidget { - final IconData icon; - final Color color; - - const ActionButton({super.key, required this.icon, required this.color}); - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: color.withValues(alpha: 0.1), - shape: BoxShape.circle, - border: Border.all(color: color, width: 2), - ), - child: Icon(icon, color: color, size: 30), - ); - } -} diff --git a/lib/features/operations/ui/operation_form_screen/operation_form_screen.dart b/lib/features/operations/ui/operation_form_screen/operation_form_screen.dart deleted file mode 100644 index b7379e3..0000000 --- a/lib/features/operations/ui/operation_form_screen/operation_form_screen.dart +++ /dev/null @@ -1,201 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.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/operation_form_screen/attachment_section.dart'; -import 'package:flux/features/operations/ui/operation_form_screen/customer_section.dart'; -import 'package:flux/features/operations/ui/operation_form_screen/general_info_section.dart'; - -class OperationFormScreen extends StatefulWidget { - final String? operationId; - final OperationModel? existingOperation; // <-- AGGIUNTO - - const OperationFormScreen({ - super.key, - this.operationId, - this.existingOperation, // <-- AGGIUNTO - }); - - @override - State createState() => _OperationFormScreenState(); -} - -class _OperationFormScreenState extends State { - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - // Diamo in pasto al Cubit tutto quello che abbiamo! - context.read().initOperationForm( - existingOperation: widget.existingOperation, - operationId: widget.operationId, - ); - }); - } - - void _performSave( - BuildContext context, { - required OperationStatus targetStatus, - required bool shouldPop, - }) { - FocusScope.of(context).unfocus(); - context.read().saveCurrentOperation( - targetStatus: targetStatus, - shouldPop: shouldPop, - ); - } - - @override - Widget build(BuildContext context) { - return BlocConsumer( - listener: (context, state) { - if (state.status == OperationsStatus.saved) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("Pratica salvata con successo!"), - backgroundColor: Colors.green, - ), - ); - Navigator.pop(context); - } - if (state.status == OperationsStatus.failure) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text("Errore: ${state.errorMessage ?? ''}"), - backgroundColor: Colors.red, - ), - ); - } - if (state.status == OperationsStatus.savedNoPop) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("Pratica salvata con successo!"), - backgroundColor: Colors.green, - ), - ); - } - }, - builder: (context, state) { - final operation = state.currentOperation; - final isSaving = state.status == OperationsStatus.saving; - final isEditMode = widget.operationId != null; - - return Scaffold( - appBar: AppBar( - title: Text(isEditMode ? "Modifica Pratica" : "Nuova Pratica"), - actions: [ - if (isSaving) - const Padding( - padding: EdgeInsets.only(right: 20.0), - child: Center( - child: SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator(strokeWidth: 2), - ), - ), - ) - else if (operation != null) ...[ - IconButton( - icon: const Icon(Icons.edit_note), - tooltip: "Salva come Bozza", - onPressed: () => _performSave( - context, - targetStatus: OperationStatus.draft, - shouldPop: false, - ), - ), - IconButton( - icon: const Icon( - Icons.check_circle_outline, - color: Colors.green, - ), - tooltip: "Conferma Pratica", - onPressed: () => _performSave( - context, - targetStatus: OperationStatus.ok, - shouldPop: true, - ), - ), - const SizedBox(width: 8), - ], - ], - ), - body: (operation == null) - ? const Center(child: CircularProgressIndicator()) - : SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CustomerSection(operation: operation), - const SizedBox(height: 24), - - GeneralInfoSection(operation: operation), - const SizedBox(height: 24), - - AttachmentsSection(), - const SizedBox(height: 32), - _buildBottomActionButtons(context, isSaving: isSaving), - const SizedBox(height: 32), - ], - ), - ), - ); - }, - ); - } - - Widget _buildBottomActionButtons( - BuildContext context, { - required bool isSaving, - }) { - return Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Expanded( - flex: 1, - child: OutlinedButton.icon( - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 16), - ), - icon: const Icon(Icons.edit_note), - label: const Text("Salva in Bozza"), - onPressed: isSaving - ? null - : () => _performSave( - context, - targetStatus: OperationStatus.draft, - shouldPop: false, - ), - ), - ), - - const SizedBox(width: 16), - - Expanded( - flex: 2, - child: ElevatedButton.icon( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green.shade600, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 16), - ), - icon: const Icon(Icons.check_circle_outline), - label: const Text( - "CONFERMA PRATICA", - style: TextStyle(fontWeight: FontWeight.bold, letterSpacing: 1), - ), - onPressed: isSaving - ? null - : () => _performSave( - context, - targetStatus: OperationStatus.ok, - shouldPop: true, - ), - ), - ), - ], - ); - } -} diff --git a/lib/features/operations/ui/operation_form_screen/operation_mobile_upload_screen.dart b/lib/features/operations/ui/operation_mobile_upload_screen.dart similarity index 100% rename from lib/features/operations/ui/operation_form_screen/operation_mobile_upload_screen.dart rename to lib/features/operations/ui/operation_mobile_upload_screen.dart index ad3fde1..730efd0 100644 --- a/lib/features/operations/ui/operation_form_screen/operation_mobile_upload_screen.dart +++ b/lib/features/operations/ui/operation_mobile_upload_screen.dart @@ -1,9 +1,9 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flux/features/operations/blocs/operation_files_bloc.dart'; import 'package:image_picker/image_picker.dart'; import 'package:file_picker/file_picker.dart'; -import 'package:flux/features/operations/blocs/operation_files_bloc.dart'; class OperationMobileUploadScreen extends StatefulWidget { final String operationId; diff --git a/lib/main.dart b/lib/main.dart index 979c1ec..9d86a5b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -16,7 +16,7 @@ import 'package:flux/core/data/core_repository.dart'; import 'package:flux/core/routes/app_router.dart'; import 'package:flux/core/theme/theme.dart'; import 'package:flux/core/theme/bloc/theme_bloc.dart'; -import 'package:flux/features/customers/blocs/customer_cubit.dart'; +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'; @@ -48,7 +48,7 @@ void main() async { // Cubit delle feature BlocProvider(create: (_) => StoreCubit()), - BlocProvider(create: (_) => CustomerCubit()), + BlocProvider(create: (_) => CustomersCubit()), BlocProvider(create: (_) => ProductCubit()), BlocProvider(create: (_) => StaffCubit()), BlocProvider(create: (_) => OperationsCubit()),