diff --git a/lib/core/routes/app_router.dart b/lib/core/routes/app_router.dart index d836fa0..47e209a 100644 --- a/lib/core/routes/app_router.dart +++ b/lib/core/routes/app_router.dart @@ -12,10 +12,11 @@ import 'package:flux/core/widgets/image_upload/ui/upload_success_screen.dart'; import 'package:flux/features/auth/ui/auth_screen.dart'; import 'package:flux/features/company/bloc/company_settings_cubit.dart'; import 'package:flux/features/company/ui/company_settings_screen.dart'; -import 'package:flux/features/customers/blocs/customers_cubit.dart'; +import 'package:flux/features/customers/blocs/customer_form_cubit.dart'; +import 'package:flux/features/customers/blocs/customers_list_cubit.dart'; import 'package:flux/features/customers/models/customer_model.dart'; import 'package:flux/features/customers/ui/customer_detail_screen.dart'; -import 'package:flux/features/customers/ui/customer_form.dart'; +import 'package:flux/features/customers/ui/customer_form_screen.dart'; import 'package:flux/features/customers/ui/customers_list_screen.dart'; import 'package:flux/features/home/ui/home_screen.dart'; import 'package:flux/features/master_data/master_data_hub_content.dart'; @@ -247,7 +248,7 @@ class AppRouter { TrackingParentType.ticket, ); } - context.read().loadCustomers(); + context.read().loadCustomers(); context.read().loadModels(); context.read().loadBrands(); @@ -328,13 +329,24 @@ class AppRouter { path: '/customer/form/:id', name: Routes.customerForm, builder: (context, state) { + final String pathId = state.pathParameters['id'] ?? 'new'; + final String? realCustomerId; + if (pathId == 'new') { + realCustomerId = null; + } else { + realCustomerId = pathId; + } final customer = state.extra as CustomerModel?; + return BlocProvider( - create: (context) => AttachmentsBloc( - parentType: AttachmentParentType.customer, - parentId: customer.id, + create: (context) => CustomerFormCubit( + existingCustomer: customer, + customerId: realCustomerId, + ), + child: CustomerFormScreen( + customer: customer, + customerId: realCustomerId, ), - child: CustomerForm(customer: customer), ); }, ), @@ -365,7 +377,7 @@ class AppRouter { .state .currentStore! .id!; - context.read().loadCustomers(); + context.read().loadCustomers(); context.read().loadProviders(currentStoreId); context.read().loadModels(); context.read().loadBrands(); diff --git a/lib/core/widgets/set_password_screen.dart b/lib/core/widgets/set_password_screen.dart index 5f5d72d..0dc103d 100644 --- a/lib/core/widgets/set_password_screen.dart +++ b/lib/core/widgets/set_password_screen.dart @@ -1,4 +1,6 @@ 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/flux_text_field.dart'; import 'package:get_it/get_it.dart'; @@ -72,6 +74,12 @@ class _SetPasswordScreenState extends State { title: Text(context.l10n.setPasswordScreenWelcomeInFlux), automaticallyImplyLeading: false, // Non può tornare indietro, deve mettere la password! + actions: [ + IconButton.filled( + onPressed: () => context.read().signOut(), + icon: Icon(Icons.logout), + ), + ], ), body: Padding( padding: const EdgeInsets.all(24.0), diff --git a/lib/core/widgets/shared_forms/attachments_section.dart b/lib/core/widgets/shared_forms/attachments_section.dart index 450ef0e..c2dcf41 100644 --- a/lib/core/widgets/shared_forms/attachments_section.dart +++ b/lib/core/widgets/shared_forms/attachments_section.dart @@ -426,7 +426,7 @@ class _SharedAttachmentsSectionState extends State { color: theme.colorScheme.primary, ), title: const Text( - 'Cartella Export (Es. TIM AttachmentRepository)', + 'Cartella Export PDF', style: TextStyle(fontWeight: FontWeight.bold), ), subtitle: Text( diff --git a/lib/core/widgets/shared_forms/customer_section.dart b/lib/core/widgets/shared_forms/customer_section.dart index 271bdd5..d24fd49 100644 --- a/lib/core/widgets/shared_forms/customer_section.dart +++ b/lib/core/widgets/shared_forms/customer_section.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/routes/routes.dart'; -import 'package:flux/features/customers/blocs/customers_cubit.dart'; +import 'package:flux/features/customers/blocs/customers_list_cubit.dart'; import 'package:flux/features/customers/models/customer_model.dart'; import 'package:flux/features/customers/ui/quick_customer_dialog.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -253,7 +253,7 @@ class SharedCustomerSection extends StatelessWidget { ), onChanged: (query) { currentSearchQuery = query; - context.read().searchCustomers(query); + context.read().searchCustomers(query); }, ), ), @@ -272,7 +272,7 @@ class SharedCustomerSection extends StatelessWidget { context: context, builder: (dialogContext) { return BlocProvider.value( - value: context.read(), + value: context.read(), child: QuickCustomerDialog( initialQuery: currentSearchQuery, // <-- Passiamo quello che ha digitato! @@ -297,9 +297,9 @@ class SharedCustomerSection extends StatelessWidget { const Divider(), // Lista Clienti dal Bloc Expanded( - child: BlocBuilder( + child: BlocBuilder( builder: (context, state) { - if (state.status == CustomersStatus.loading) { + if (state.status == CustomersListStatus.loading) { return const Center(child: CircularProgressIndicator()); } if (state.customers.isEmpty) { diff --git a/lib/features/customers/blocs/customer_form_cubit.dart b/lib/features/customers/blocs/customer_form_cubit.dart index cc4c6d2..1eae975 100644 --- a/lib/features/customers/blocs/customer_form_cubit.dart +++ b/lib/features/customers/blocs/customer_form_cubit.dart @@ -11,7 +11,7 @@ class CustomerFormCubit extends Cubit { final CustomerRepository _repository = GetIt.I(); final SessionCubit _sessionCubit = GetIt.I(); - CustomerFormCubit({CustomerModel? existingCustomer}) + CustomerFormCubit({CustomerModel? existingCustomer, String? customerId}) : super( CustomerFormState(customer: existingCustomer ?? CustomerModel.empty()), ); @@ -103,4 +103,26 @@ class CustomerFormCubit extends Cubit { ); } } + + Future quickCreateCustomer({ + required String name, + String? phone, + String? email, + }) async { + final newCustomer = CustomerModel( + name: name, + phoneNumber: phone ?? '', + email: email ?? '', + companyId: _sessionCubit.state.company!.id!, + note: '', + ); + + try { + final saved = await _repository.insertCustomer(newCustomer); + // Lo aggiungeremo in cima ai suggerimenti + return saved; + } catch (e) { + return null; + } + } } diff --git a/lib/features/customers/blocs/customers_cubit.dart b/lib/features/customers/blocs/customers_cubit.dart deleted file mode 100644 index 44e58d5..0000000 --- a/lib/features/customers/blocs/customers_cubit.dart +++ /dev/null @@ -1,160 +0,0 @@ -import 'dart:async'; // Serve per il Timer del debounce -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:equatable/equatable.dart'; -import 'package:flux/core/blocs/session/session_cubit.dart'; -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 'customers_state.dart'; - -class CustomersCubit extends Cubit { - final CustomerRepository _repository = GetIt.I(); - final SessionCubit _sessionCubit = GetIt.I(); - - // Variabile per gestire il debounce della ricerca - Timer? _searchDebounce; - - CustomersCubit() : super(const CustomersState()); - - // --- LETTURA --- - Future loadCustomers() async { - emit(state.copyWith(status: CustomersStatus.loading)); - try { - final customers = await _repository.getCustomers( - _sessionCubit.state.company!.id!, - ); - emit( - state.copyWith(status: CustomersStatus.success, customers: customers), - ); - } catch (e) { - emit( - state.copyWith( - status: CustomersStatus.failure, - errorMessage: e.toString(), - ), - ); - } - } - - // --- CREAZIONE --- - Future createCustomer(CustomerModel customer) async { - emit(state.copyWith(status: CustomersStatus.loading)); - try { - final newCustomer = await _repository.saveCustomer(customer); - - // Aggiorniamo la lista locale aggiungendo il nuovo cliente in cima - final updatedList = List.from(state.customers) - ..insert(0, newCustomer); - - emit( - state.copyWith( - status: CustomersStatus.success, - customers: updatedList, - lastCreatedCustomer: newCustomer, - ), - ); - } catch (e) { - emit( - state.copyWith( - status: CustomersStatus.failure, - errorMessage: e.toString(), - ), - ); - } - } - - // --- AGGIORNAMENTO --- - Future updateCustomer(CustomerModel customer) async { - emit(state.copyWith(status: CustomersStatus.loading)); - try { - final updatedCustomer = await _repository.updateCustomer(customer); - - final updatedList = List.from(state.customers); - final index = updatedList.indexWhere((c) => c.id == updatedCustomer.id); - - if (index != -1) { - updatedList[index] = updatedCustomer; - } - - emit( - state.copyWith( - status: CustomersStatus.success, - customers: updatedList, - lastCreatedCustomer: - updatedCustomer, // Utile se modifichi un cliente appena creato - ), - ); - } catch (e) { - emit( - state.copyWith( - status: CustomersStatus.failure, - errorMessage: e.toString(), - ), - ); - } - } - - // --- RICERCA CON DEBOUNCE --- - void searchCustomers(String query) { - // 1. Se c'è già una ricerca in attesa (l'utente sta digitando veloce), la annulliamo - if (_searchDebounce?.isActive ?? false) _searchDebounce!.cancel(); - - // 2. Facciamo partire un timer di 400 millisecondi - _searchDebounce = Timer(const Duration(milliseconds: 300), () async { - // Se cancella tutto e la query è vuota, ricarichiamo la lista base - if (query.trim().isEmpty) { - await loadCustomers(); - return; - } - - // Nessun "loading" state qui, per evitare sfarfallii visivi mentre si scrive - try { - final results = await _repository.searchCustomers( - _sessionCubit.state.company!.id!, - query, - ); - emit( - state.copyWith(status: CustomersStatus.success, customers: results), - ); - } catch (e) { - emit( - state.copyWith( - status: CustomersStatus.failure, - errorMessage: e.toString(), - ), - ); - } - }); - } - - Future quickCreateCustomer({ - required String name, - String? phone, - String? email, - }) async { - final newCustomer = CustomerModel( - name: name, - phoneNumber: phone ?? '', - email: email ?? '', - companyId: _sessionCubit.state.company!.id!, - note: '', - ); - - try { - final saved = await _repository.saveCustomer(newCustomer); - // Lo aggiungiamo in cima ai suggerimenti - emit(state.copyWith(customers: [saved, ...state.customers])); - return saved; - } catch (e) { - return null; - } - } - - // Pulizia della memoria quando il Cubit viene distrutto - @override - Future close() { - _searchDebounce?.cancel(); - return super.close(); - } -} diff --git a/lib/features/customers/blocs/customers_list_cubit.dart b/lib/features/customers/blocs/customers_list_cubit.dart new file mode 100644 index 0000000..f822c02 --- /dev/null +++ b/lib/features/customers/blocs/customers_list_cubit.dart @@ -0,0 +1,85 @@ +import 'dart:async'; // Serve per il Timer del debounce +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flux/core/blocs/session/session_cubit.dart'; +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 'customers_list_state.dart'; + +class CustomersListCubit extends Cubit { + final CustomerRepository _repository = GetIt.I(); + final SessionCubit _sessionCubit = GetIt.I(); + + // Variabile per gestire il debounce della ricerca + Timer? _searchDebounce; + + CustomersListCubit() : super(const CustomersListState()); + + // --- LETTURA --- + Future loadCustomers() async { + emit(state.copyWith(status: CustomersListStatus.loading)); + try { + final customers = await _repository.getCustomers( + _sessionCubit.state.company!.id!, + ); + emit( + state.copyWith( + status: CustomersListStatus.success, + customers: customers, + ), + ); + } catch (e) { + emit( + state.copyWith( + status: CustomersListStatus.failure, + errorMessage: e.toString(), + ), + ); + } + } + + // --- RICERCA CON DEBOUNCE --- + void searchCustomers(String query) { + // 1. Se c'è già una ricerca in attesa (l'utente sta digitando veloce), la annulliamo + if (_searchDebounce?.isActive ?? false) _searchDebounce!.cancel(); + + // 2. Facciamo partire un timer di 400 millisecondi + _searchDebounce = Timer(const Duration(milliseconds: 300), () async { + // Se cancella tutto e la query è vuota, ricarichiamo la lista base + if (query.trim().isEmpty) { + await loadCustomers(); + return; + } + + // Nessun "loading" state qui, per evitare sfarfallii visivi mentre si scrive + try { + final results = await _repository.searchCustomers( + _sessionCubit.state.company!.id!, + query, + ); + emit( + state.copyWith( + status: CustomersListStatus.success, + customers: results, + ), + ); + } catch (e) { + emit( + state.copyWith( + status: CustomersListStatus.failure, + errorMessage: e.toString(), + ), + ); + } + }); + } + + // Pulizia della memoria quando il Cubit viene distrutto + @override + Future close() { + _searchDebounce?.cancel(); + return super.close(); + } +} diff --git a/lib/features/customers/blocs/customers_state.dart b/lib/features/customers/blocs/customers_list_state.dart similarity index 70% rename from lib/features/customers/blocs/customers_state.dart rename to lib/features/customers/blocs/customers_list_state.dart index 453aaf7..e43714f 100644 --- a/lib/features/customers/blocs/customers_state.dart +++ b/lib/features/customers/blocs/customers_list_state.dart @@ -1,6 +1,6 @@ -part of 'customers_cubit.dart'; +part of 'customers_list_cubit.dart'; -enum CustomersStatus { +enum CustomersListStatus { initial, loading, filesLoading, @@ -9,26 +9,26 @@ enum CustomersStatus { failure, } -class CustomersState extends Equatable { - final CustomersStatus status; +class CustomersListState extends Equatable { + final CustomersListStatus status; final List customers; final CustomerModel? lastCreatedCustomer; final String? errorMessage; - const CustomersState({ - this.status = CustomersStatus.initial, + const CustomersListState({ + this.status = CustomersListStatus.initial, this.customers = const [], this.lastCreatedCustomer, this.errorMessage, }); - CustomersState copyWith({ - CustomersStatus? status, + CustomersListState copyWith({ + CustomersListStatus? status, List? customers, CustomerModel? lastCreatedCustomer, String? errorMessage, }) { - return CustomersState( + return CustomersListState( status: status ?? this.status, customers: customers ?? this.customers, lastCreatedCustomer: lastCreatedCustomer ?? this.lastCreatedCustomer, diff --git a/lib/features/customers/ui/customer_form.dart b/lib/features/customers/ui/customer_form_screen.dart similarity index 81% rename from lib/features/customers/ui/customer_form.dart rename to lib/features/customers/ui/customer_form_screen.dart index 7ee285d..6f61e86 100644 --- a/lib/features/customers/ui/customer_form.dart +++ b/lib/features/customers/ui/customer_form_screen.dart @@ -1,20 +1,22 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/widgets/flux_text_field.dart'; +import 'package:flux/core/widgets/shared_forms/attachments_section.dart'; +import 'package:flux/features/attachments/blocs/attachments_bloc.dart'; import 'package:flux/features/customers/blocs/customer_form_cubit.dart'; import 'package:flux/features/customers/models/customer_model.dart'; // Uso il tuo widget! -class CustomerForm extends StatefulWidget { +class CustomerFormScreen extends StatefulWidget { final CustomerModel? customer; final String? customerId; - const CustomerForm({super.key, this.customer, this.customerId}); + const CustomerFormScreen({super.key, this.customer, this.customerId}); @override - State createState() => _CustomerFormState(); + State createState() => _CustomerFormScreenState(); } -class _CustomerFormState extends State { +class _CustomerFormScreenState extends State { final _formKey = GlobalKey(); // Controller inizializzati con i dati del cliente (se presenti) @@ -27,10 +29,18 @@ class _CustomerFormState extends State { @override void initState() { super.initState(); + + // 1. Lanciamo l'inizializzazione (che se è sincrona, finirà istantaneamente) context.read().initForm( customerId: widget.customerId, existingCustomer: widget.customer, ); + + // 2. Leggiamo lo stato SUBITO DOPO. Se è già ready, non aspettiamo il listener! + final currentState = context.read().state; + if (currentState.status == CustomerFormStatus.ready && !_isInitialized) { + _syncTextControllers(currentState.customer); + } } @override @@ -144,6 +154,17 @@ class _CustomerFormState extends State { .read() .updateDoNotDisturb(v), ), + const Divider(height: 32), + BlocProvider( + create: (context) => AttachmentsBloc( + parentType: AttachmentParentType.customer, + parentId: state.customer.id, + ), + child: SharedAttachmentsSection( + parentType: AttachmentParentType.customer, + parentId: state.customer.id, + ), + ), const SizedBox(height: 24), SizedBox( width: double.infinity, diff --git a/lib/features/customers/ui/customers_list_screen.dart b/lib/features/customers/ui/customers_list_screen.dart index 7c3a46d..cd230b8 100644 --- a/lib/features/customers/ui/customers_list_screen.dart +++ b/lib/features/customers/ui/customers_list_screen.dart @@ -3,9 +3,8 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/routes/routes.dart'; import 'package:flux/core/theme/theme.dart'; -import 'package:flux/features/customers/blocs/customers_cubit.dart'; +import 'package:flux/features/customers/blocs/customers_list_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'; class CustomersListScreen extends StatefulWidget { @@ -27,14 +26,14 @@ class _CustomersListScreenState 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); } } @@ -53,7 +52,12 @@ class _CustomersListScreenState extends State { Padding( padding: const EdgeInsets.only(right: 16), child: ElevatedButton.icon( - onPressed: () => openCustomerForm(context: context), + onPressed: () { + context.pushNamed( + Routes.customerForm, + pathParameters: {'id': 'new'}, + ); + }, icon: const Icon(Icons.person_add_alt_1_rounded, size: 20), label: const Text('NUOVO'), style: ElevatedButton.styleFrom( @@ -87,9 +91,9 @@ class _CustomersListScreenState extends State { // LISTA CLIENTI Expanded( - child: BlocBuilder( + child: BlocBuilder( builder: (context, state) { - if (state.status == CustomersStatus.loading && + if (state.status == CustomersListStatus.loading && state.customers.isEmpty) { return const Center(child: CircularProgressIndicator()); } @@ -214,8 +218,16 @@ class _CustomerTile extends StatelessWidget { ), ), trailing: IconButton( - onPressed: () => - openCustomerForm(context: context, customer: customer), + onPressed: () async { + final CustomersListCubit customersCubit = context + .read(); + await context.pushNamed( + Routes.customerForm, + pathParameters: {'id': customer.id!}, + extra: customer, + ); + customersCubit.loadCustomers(); + }, icon: Icon(Icons.edit_note_rounded, color: context.accent), ), ), @@ -224,7 +236,7 @@ class _CustomerTile extends StatelessWidget { } /// Funzione unica per gestire Creazione e Modifica -void openCustomerForm({ +/* void openCustomerForm({ CustomerModel? customer, required BuildContext context, }) { @@ -257,4 +269,4 @@ void openCustomerForm({ ), ), ); -} +} */ diff --git a/lib/features/customers/ui/quick_customer_dialog.dart b/lib/features/customers/ui/quick_customer_dialog.dart index 2137dee..60de695 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/customers_cubit.dart'; +import 'package:flux/features/customers/blocs/customer_form_cubit.dart'; class QuickCustomerDialog extends StatefulWidget { final String initialQuery; @@ -43,13 +43,11 @@ class _QuickCustomerDialogState extends State { // Chiamata al Cubit (aggiorna i parametri in base a come li hai definiti) final newCustomer = await context - .read() + .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(), + email: _emailCtrl.text.trim(), ); setState(() => _isLoading = false); diff --git a/lib/features/onboarding/ui/onboarding_screen.dart b/lib/features/onboarding/ui/onboarding_screen.dart index 926c894..3438755 100644 --- a/lib/features/onboarding/ui/onboarding_screen.dart +++ b/lib/features/onboarding/ui/onboarding_screen.dart @@ -84,6 +84,14 @@ class _OnboardingScreenState extends State { }, builder: (context, state) { return Scaffold( + appBar: AppBar( + actions: [ + IconButton.filled( + onPressed: () => context.read().signOut(), + icon: const Icon(Icons.logout), + ), + ], + ), body: SafeArea( child: Stack( children: [ diff --git a/lib/features/operations/ui/operation_form_screen.dart b/lib/features/operations/ui/operation_form_screen.dart index 3556110..11eb867 100644 --- a/lib/features/operations/ui/operation_form_screen.dart +++ b/lib/features/operations/ui/operation_form_screen.dart @@ -5,8 +5,7 @@ import 'package:flux/features/operations/blocs/operation_form_cubit.dart'; import 'package:flux/features/operations/models/operation_model.dart'; import 'package:flux/core/widgets/shared_forms/customer_section.dart'; import 'package:flux/features/operations/ui/widgets/details_section.dart'; -import 'package:flux/core/widgets/shared_forms/attachments_section.dart'; -import 'package:flux/core/widgets/shared_forms/staff_section.dart'; +import 'package:flux/core/widgets/shared_forms/shared_files_section.dart'; // <- Cambiato ad un file unico per coerenza col ticket class OperationFormScreen extends StatefulWidget { final String? operationId; @@ -35,6 +34,7 @@ class _OperationFormScreenState extends State { 'MNP', 'NIP', 'UNICA', + 'FWA', 'TELEPASS', 'Energy', 'Fin', @@ -47,10 +47,17 @@ class _OperationFormScreenState extends State { @override void initState() { super.initState(); + // 1. Lanciamo l'inizializzazione sincrona/asincrona context.read().initForm( existingOperation: widget.existingOperation, operationId: widget.operationId, ); + + // 2. Lettura immediata dello stato (come fatto per il customer!) + final currentState = context.read().state; + if (currentState.status == OperationFormStatus.ready && !_isInitialized) { + _syncTextControllers(currentState.operation); + } } @override @@ -75,16 +82,6 @@ class _OperationFormScreenState extends State { if (_freeTextDescriptionController.text.isEmpty) { _freeTextDescriptionController.text = model.description ?? ''; } - - // Se è una nuova pratica (draft), impostiamo di default il target su OK per comodità UI - if (model.id == null && model.status == OperationStatus.draft) { - // Usiamo addPostFrameCallback per non interferire con il build attuale - WidgetsBinding.instance.addPostFrameCallback((_) { - // Supponendo tu aggiunga la possibilità di aggiornare lo status nel metodo updateFields del Cubit - // context.read().updateFields(status: OperationStatus.ok); - }); - } - _isInitialized = true; } @@ -103,6 +100,9 @@ class _OperationFormScreenState extends State { }) { if (_formKey.currentState!.validate()) { _flushControllersToCubit(); + // Aggiorniamo prima lo stato bersaglio nel cubit + context.read().updateFields(status: targetStatus); + // Poi chiamiamo il salvataggio context.read().saveOperation( targetStatus: targetStatus, keepAdding: keepAdding, @@ -114,7 +114,8 @@ class _OperationFormScreenState extends State { if (!_formKey.currentState!.validate()) return null; _flushControllersToCubit(); final attachmentsBloc = context.read(); - // Presumo tu abbia creato il metodo saveOperationDraft() nel Cubit! + + // Assicurati che questo metodo esista nel Cubit (come per il Ticket) final newId = await context.read().saveOperationDraft(); if (newId != null && context.mounted) { attachmentsBloc.add(ParentEntitySavedEvent(newId)); @@ -122,7 +123,6 @@ class _OperationFormScreenState extends State { return newId; } - // Helper per assegnare un colore agli stati Color _getStatusColor(OperationStatus status) { switch (status) { case OperationStatus.success: @@ -158,6 +158,7 @@ class _OperationFormScreenState extends State { ); _freeTextSubtypeController.clear(); _freeTextDescriptionController.clear(); + _referenceController.clear(); } else if (state.status == OperationFormStatus.failure) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -176,8 +177,6 @@ class _OperationFormScreenState extends State { ); } - // Determiniamo lo stato da mostrare nel form. - // Se è una bozza appena creata, mostriamo visivamente "OK" come default per il salvataggio. final displayStatus = state.operation.status == OperationStatus.draft && state.operation.id == null @@ -191,7 +190,6 @@ class _OperationFormScreenState extends State { ? 'Nuova Pratica - Operatore: ${state.operation.staffDisplayName}' : 'Modifica Pratica - Operatore: ${state.operation.staffDisplayName}', ), - // Mettiamo un piccolo indicatore visivo anche nella AppBar se non è OK actions: displayStatus != OperationStatus.success && displayStatus != OperationStatus.draft @@ -214,64 +212,54 @@ class _OperationFormScreenState extends State { ), body: Form( key: _formKey, - child: LayoutBuilder( - builder: (context, constraints) { - final isUltraWide = constraints.maxWidth > 1400; - final isDesktop = constraints.maxWidth > 900; - if (isUltraWide) { - return _buildUltraWide(state, theme); - } else if (isDesktop) { - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - flex: 7, - child: SingleChildScrollView( - padding: const EdgeInsets.all(16.0), - child: _buildMainFormContent( - theme, - state, - displayStatus, - ), - ), - ), - VerticalDivider(width: 1, color: theme.dividerColor), - Expanded( - flex: 3, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: _buildNotesSection(isDesktop: true), - ), - ), - ], - ); - } else { + child: FocusTraversalGroup( + policy: WidgetOrderTraversalPolicy(), + child: LayoutBuilder( + builder: (context, constraints) { + final isUltraWide = constraints.maxWidth > 1400; + final isDesktop = constraints.maxWidth > 900; + return SingleChildScrollView( padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildMainFormContent(theme, state, displayStatus), - const Divider(height: 32), - _buildNotesSection(isDesktop: false), - const SizedBox(height: 80), - ], + child: Center( + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: isUltraWide + ? 1600 + : (isDesktop ? 1200 : 800), + ), + child: _buildResponsiveLayout( + isUltraWide, + isDesktop, + state, + displayStatus, + ), + ), ), ); - } - }, + }, + ), ), ), bottomNavigationBar: SafeArea( - child: Padding( + child: Container( padding: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + color: theme.scaffoldBackgroundColor, + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, -3), + ), + ], + ), child: Row( children: [ Expanded( flex: 1, child: ElevatedButton( style: ElevatedButton.styleFrom( - // Se c'è un KO o un blocco, cambiamo il colore del bottone principale per attirare l'attenzione backgroundColor: displayStatus != OperationStatus.success && displayStatus != OperationStatus.draft @@ -287,8 +275,7 @@ class _OperationFormScreenState extends State { ? null : () => _saveOperation( keepAdding: false, - targetStatus: - displayStatus, // <-- Usiamo lo stato selezionato nel form! + targetStatus: displayStatus, ), child: state.status == OperationFormStatus.saving ? const SizedBox( @@ -302,7 +289,6 @@ class _OperationFormScreenState extends State { : const Text('Salva ed Esci'), ), ), - const SizedBox(width: 12), Expanded( flex: 1, @@ -311,8 +297,7 @@ class _OperationFormScreenState extends State { ? null : () => _saveOperation( keepAdding: true, - targetStatus: - displayStatus, // <-- Usiamo lo stato selezionato nel form! + targetStatus: displayStatus, ), child: const Text( 'Salva e Aggiungi Altro', @@ -329,72 +314,100 @@ class _OperationFormScreenState extends State { ); } - Widget _buildUltraWide(OperationFormState state, ThemeData theme) { - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - flex: 4, - child: SingleChildScrollView( - padding: const EdgeInsets.all(16.0), + // --- LOGICA DI IMPAGINAZIONE RESPONSIVE --- + Widget _buildResponsiveLayout( + bool isUltraWide, + bool isDesktop, + OperationFormState state, + OperationStatus displayStatus, + ) { + if (isUltraWide) { + // 3 COLONNE + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + children: [_cardAnagrafica(state), _cardEsito(state)], + ), + ), + const SizedBox(width: 24), + Expanded(child: Column(children: [_cardDettagli(state)])), + const SizedBox(width: 24), + Expanded( + child: Column(children: [_cardNote(state), _cardAllegati(state)]), + ), + ], + ); + } else if (isDesktop) { + // 2 COLONNE + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( child: Column( - crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildStaffSection(state), - const Divider(height: 50), - _buildOperationStatusSection(state), - const Divider(height: 32), - _buildCustomerSection(state), - const SizedBox(height: 16), - _buildReferenceSection(state), - const Divider(height: 50), - _buildOperationTypeSection(state), - const SizedBox(height: 16), - _buildQuantitySection(state), - const Divider(height: 50), - _buildDetailsSection(state), - const Divider(height: 50), + _cardAnagrafica(state), + _cardEsito(state), + _cardAllegati(state), ], ), ), - ), - VerticalDivider(width: 1, color: theme.dividerColor), - Expanded( - flex: 3, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: _buildNotesSection(isDesktop: true), + const SizedBox(width: 24), + Expanded( + child: Column(children: [_cardDettagli(state), _cardNote(state)]), ), + ], + ); + } else { + // 1 COLONNA (Mobile) + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _cardAnagrafica(state), + _cardEsito(state), + _cardDettagli(state), + _cardNote(state), + _cardAllegati(state), + ], + ); + } + } + + // --- LE CARD MODULARIZZATE E COLORATE --- + + Widget _cardAnagrafica(OperationFormState state) { + return _buildCard( + title: 'Cliente e Riferimento', + icon: Icons.person, + themeColor: Colors.indigo, + children: [ + SharedCustomerSection( + customer: state.operation.customer, + onCustomerSelected: (customer) => context + .read() + .updateFields(customer: customer), ), - VerticalDivider(width: 1, color: theme.dividerColor), - Expanded( - flex: 3, - child: SingleChildScrollView( - padding: const EdgeInsets.all(16.0), - child: _buildAttachmentSection(state), + const SizedBox(height: 16), + TextFormField( + controller: _referenceController, + decoration: const InputDecoration( + labelText: 'Riferimento (es. Telefono, Targa...)', + prefixIcon: Icon(Icons.tag), ), + validator: (v) => + v == null || v.isEmpty ? 'Inserisci un riferimento' : null, ), ], ); } - Widget _buildStaffSection(OperationFormState state) { - return StaffSection( - staffId: state.operation.staffId, - staffName: state.operation.staffDisplayName, - onStaffSelected: (staff) => { - context.read().updateFields( - staffId: staff.id, - staffDisplayName: staff.name, - ), - }, - ); - } - - Widget _buildOperationStatusSection(OperationFormState state) { - return Column( + Widget _cardEsito(OperationFormState state) { + return _buildCard( + title: 'Esito Pratica', + icon: Icons.fact_check, + themeColor: _getStatusColor(state.operation.status), children: [ - _buildSectionTitle('Esito / Stato Operazione'), Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), decoration: BoxDecoration( @@ -416,42 +429,35 @@ class _OperationFormScreenState extends State { Icons.arrow_drop_down, color: _getStatusColor(state.operation.status), ), - items: OperationStatus.values - /* .where( - (s) => s != OperationStatus.draft, - ) // Nascondiamo 'Bozza' dal menu */ - .map( - (status) => DropdownMenuItem( - value: status, - child: Row( - children: [ - Icon( - status == OperationStatus.success - ? Icons.check_circle - : Icons.error_outline, - color: _getStatusColor(status), - size: 20, - ), - const SizedBox(width: 12), - Text( - status.displayName, - style: TextStyle( - fontWeight: FontWeight.w600, - color: _getStatusColor(status), - ), - ), - ], + items: OperationStatus.values.map((status) { + return DropdownMenuItem( + value: status, + child: Row( + children: [ + Icon( + status == OperationStatus.success + ? Icons.check_circle + : Icons.error_outline, + color: _getStatusColor(status), + size: 20, ), - ), - ) - .toList(), + const SizedBox(width: 12), + Text( + status.displayName, + style: TextStyle( + fontWeight: FontWeight.w600, + color: _getStatusColor(status), + ), + ), + ], + ), + ); + }).toList(), onChanged: (newStatus) { - if (newStatus != null) { - // Assicurati che il metodo updateFields nel tuo Cubit accetti anche 'status' + if (newStatus != null) context.read().updateFields( status: newStatus, ); - } }, ), ), @@ -459,37 +465,20 @@ class _OperationFormScreenState extends State { const SizedBox(height: 8), Text( state.operation.status == OperationStatus.success - ? 'Lascia OK se la pratica è stata caricata con successo.' - : 'Attenzione: la pratica verrà salvata come ${state.operation.status.displayName}.', + ? 'Lascia OK se caricata con successo.' + : 'Attenzione: pratica salvata in stato anomalo.', style: TextStyle(fontSize: 12, color: Colors.grey.shade600), ), ], ); } - Widget _buildCustomerSection(OperationFormState state) { - return SharedCustomerSection( - customer: state.operation.customer, - onCustomerSelected: (customer) { - context.read().updateFields(customer: customer); - }, - ); - } - - Widget _buildReferenceSection(OperationFormState state) { - return TextFormField( - controller: _referenceController, - decoration: const InputDecoration( - labelText: 'Riferimento (es. numero di telefono, targa...)', - prefixIcon: Icon(Icons.tag), - ), - ); - } - - Widget _buildOperationTypeSection(OperationFormState state) { - return Column( + Widget _cardDettagli(OperationFormState state) { + return _buildCard( + title: 'Dettagli Servizio', + icon: Icons.design_services, + themeColor: Colors.deepOrange, children: [ - _buildSectionTitle('Cosa stiamo facendo?'), Wrap( spacing: 8.0, runSpacing: 8.0, @@ -498,104 +487,140 @@ class _OperationFormScreenState extends State { label: Text(type), selected: state.operation.type == type, onSelected: (selected) { - if (selected) { + if (selected) context.read().setTypeWithSmartDefault( type, ); - } }, ); }).toList(), ), - ], - ); - } - - Widget _buildDetailsSection(OperationFormState state) { - return Column( - children: [ - _buildSectionTitle('Dettagli Servizio'), + const Divider(height: 32), + Row( + children: [ + const Text( + 'Quantità:', + style: TextStyle(fontWeight: FontWeight.bold), + ), + const Spacer(), + IconButton( + icon: const Icon(Icons.remove_circle_outline), + onPressed: state.operation.quantity > 1 + ? () => context.read().updateFields( + quantity: state.operation.quantity - 1, + ) + : null, + ), + Text( + '${state.operation.quantity}', + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + IconButton( + icon: const Icon(Icons.add_circle_outline), + onPressed: () => context.read().updateFields( + quantity: state.operation.quantity + 1, + ), + ), + ], + ), + const Divider(height: 32), OperationDetailsSection( currentOp: state.operation, currentType: state.operation.type, freeTextSubtypeController: _freeTextSubtypeController, freeTextDescriptionController: _freeTextDescriptionController, - durationQuickPicks: _buildDurationQuickPicks(state.operation), + durationQuickPicks: _buildDurationQuickPicks(), ), ], ); } - Widget _buildQuantitySection(OperationFormState state) { - return Row( + Widget _cardNote(OperationFormState state) { + return _buildCard( + title: 'Note Interne', + icon: Icons.notes, + themeColor: Colors.teal, children: [ - const Text('Quantità: '), - IconButton( - icon: const Icon(Icons.remove), - onPressed: () { - final q = state.operation.quantity; - if (q > 1) { - context.read().updateFields(quantity: q - 1); - } - }, - ), - Text( - '${state.operation.quantity}', - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - IconButton( - icon: const Icon(Icons.add), - onPressed: () { - final q = state.operation.quantity; - context.read().updateFields(quantity: q + 1); - }, + TextFormField( + controller: _noteController, + maxLines: 5, + decoration: InputDecoration( + hintText: 'Incolla seriali, ICCID, IBAN...', + alignLabelWithHint: true, + fillColor: Colors.teal.withValues(alpha: 0.05), + filled: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + ), ), ], ); } - Widget _buildAttachmentSection(OperationFormState state) { - return SharedAttachmentsSection( - parentType: AttachmentParentType.operation, - parentId: state.operation.id, - titleForUpload: state.operation.customer?.name ?? 'Nuova pratica', - onGenerateIdForQr: _generateIdForQr, + Widget _cardAllegati(OperationFormState state) { + return _buildCard( + title: 'Allegati e Documenti', + icon: Icons.attach_file, + themeColor: Colors.deepPurple, + children: [ + SharedFilesSection( + titleNameForUpload: state.operation.customer?.name ?? 'Nuova Pratica', + onGenerateIdForQr: _generateIdForQr, + ), + ], ); } - Widget _buildMainFormContent( - ThemeData theme, - OperationFormState state, - OperationStatus displayStatus, { - bool showFiles = true, + // --- WIDGET BASE PER LA CARD --- + Widget _buildCard({ + required String title, + required IconData icon, + required Color themeColor, + required List children, }) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - /* _buildStaffSection(state), - const Divider(height: 50), */ - _buildOperationStatusSection(state), - const Divider(height: 32), - _buildCustomerSection(state), - const SizedBox(height: 16), - _buildReferenceSection(state), - const Divider(height: 50), - _buildOperationTypeSection(state), - const SizedBox(height: 16), - _buildQuantitySection(state), - const Divider(height: 50), - _buildDetailsSection(state), - const Divider(height: 50), - - // QUANTITÀ - const Divider(height: 32), - - if (showFiles) ...[_buildAttachmentSection(state)], - ], + return Card( + margin: const EdgeInsets.only(bottom: 24), + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: BorderSide(color: themeColor.withValues(alpha: 0.3), width: 1), + ), + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: themeColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon(icon, color: themeColor), + ), + const SizedBox(width: 12), + Text( + title, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + color: themeColor, + ), + ), + ], + ), + const Divider(height: 32), + ...children, + ], + ), + ), ); } - Widget _buildDurationQuickPicks(OperationModel? currentOp) { + Widget _buildDurationQuickPicks() { final durations = [3, 6, 12, 24, 30, 36, 48]; return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -636,46 +661,4 @@ class _OperationFormScreenState extends State { ], ); } - - 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(), - ), - ); - return isDesktop - ? Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - title, - const SizedBox(height: 8), - Expanded(child: noteField), - ], - ) - : Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [title, const SizedBox(height: 8), noteField], - ); - } - - Widget _buildSectionTitle(String title) { - return Padding( - padding: const EdgeInsets.only(bottom: 12.0), - child: Text( - title, - style: Theme.of( - context, - ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), - ), - ); - } } diff --git a/lib/features/operations/ui/widgets/details_section.dart b/lib/features/operations/ui/widgets/details_section.dart index 5866ff7..586d6bb 100644 --- a/lib/features/operations/ui/widgets/details_section.dart +++ b/lib/features/operations/ui/widgets/details_section.dart @@ -33,7 +33,7 @@ class OperationDetailsSection extends StatelessWidget { switch (operationType) { case 'AL' || 'MNP': return provider.roles.contains(ProviderRole.mobile); - case 'NIP': + case 'NIP' || 'FWA': return provider.roles.contains(ProviderRole.landline); case 'UNICA': return provider.roles.contains(ProviderRole.landline) || diff --git a/lib/features/tickets/blocs/ticket_shipping_cubit.dart b/lib/features/tickets/blocs/ticket_shipping_cubit.dart index 4c53b4b..7d6f1ac 100644 --- a/lib/features/tickets/blocs/ticket_shipping_cubit.dart +++ b/lib/features/tickets/blocs/ticket_shipping_cubit.dart @@ -160,7 +160,7 @@ class TicketShippingCubit extends Cubit { await _repository.createShipmentWithPdf( document: state.document, pdfBytes: pdfBytes, - newTicketStatus: newTicketStatus.value, + newTicketStatus: newTicketStatus, ); await Printing.layoutPdf( onLayout: (format) async => pdfBytes, diff --git a/lib/features/tickets/data/tickets_shipping_repository.dart b/lib/features/tickets/data/tickets_shipping_repository.dart index 51ef640..02be140 100644 --- a/lib/features/tickets/data/tickets_shipping_repository.dart +++ b/lib/features/tickets/data/tickets_shipping_repository.dart @@ -6,6 +6,7 @@ import 'package:flux/features/attachments/data/attachments_repository.dart'; import 'package:flux/features/tickets/models/shipping_document_model.dart'; import 'package:flux/features/master_data/providers/models/provider_model.dart'; import 'package:flux/features/master_data/providers/models/provider_role.dart'; +import 'package:flux/features/tickets/models/ticket_model.dart'; import 'package:get_it/get_it.dart'; import 'package:supabase_flutter/supabase_flutter.dart' hide Bucket; @@ -90,7 +91,7 @@ class TicketsShippingRepository { Future createShipmentWithPdf({ required ShippingDocumentModel document, required Uint8List pdfBytes, - required String newTicketStatus, + required TicketStatus newTicketStatus, }) async { try { final attachmentsRepo = GetIt.I.get(); @@ -124,8 +125,8 @@ class TicketsShippingRepository { await _supabase .from('ticket') .update({ - 'ticket_status': newTicketStatus, - 'shipment_document_id': documentId, + 'ticket_status': newTicketStatus.value, + 'shipping_document_id': documentId, }) .inFilter('id', document.ticketIds); } catch (e) { diff --git a/lib/main.dart b/lib/main.dart index 397017e..bab8741 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -27,7 +27,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/customers_cubit.dart'; +import 'package:flux/features/customers/blocs/customers_list_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'; @@ -59,7 +59,7 @@ void main() async { // Cubit delle feature BlocProvider(create: (_) => StoreCubit()), - BlocProvider(create: (_) => CustomersCubit()), + BlocProvider(create: (_) => CustomersListCubit()), BlocProvider(create: (_) => ProductsCubit()), BlocProvider( create: (_) => StaffCubit()