diff --git a/lib/core/routes/app_router.dart b/lib/core/routes/app_router.dart index fcf9a0f..d836fa0 100644 --- a/lib/core/routes/app_router.dart +++ b/lib/core/routes/app_router.dart @@ -15,7 +15,8 @@ import 'package:flux/features/company/ui/company_settings_screen.dart'; import 'package:flux/features/customers/blocs/customers_cubit.dart'; import 'package:flux/features/customers/models/customer_model.dart'; import 'package:flux/features/customers/ui/customer_detail_screen.dart'; -import 'package:flux/features/customers/ui/customers_content.dart'; +import 'package:flux/features/customers/ui/customer_form.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'; import 'package:flux/features/master_data/products/blocs/product_cubit.dart'; @@ -191,7 +192,7 @@ class AppRouter { path: '/customers', name: Routes.customers, builder: (context, state) => - const CustomersContent(), // O come si chiama il tuo widget della lista! + const CustomersListScreen(), // O come si chiama il tuo widget della lista! ), GoRoute( path: '/tickets', @@ -310,8 +311,8 @@ class AppRouter { builder: (context, state) => const UploadSuccessScreen(), ), GoRoute( - path: '/customer/form/:id', - name: 'customer-form', + path: '/customer/details/:id', + name: Routes.customerDetails, builder: (context, state) { final customer = state.extra as CustomerModel; return BlocProvider( @@ -323,6 +324,20 @@ class AppRouter { ); }, ), + GoRoute( + path: '/customer/form/:id', + name: Routes.customerForm, + builder: (context, state) { + final customer = state.extra as CustomerModel?; + return BlocProvider( + create: (context) => AttachmentsBloc( + parentType: AttachmentParentType.customer, + parentId: customer.id, + ), + child: CustomerForm(customer: customer), + ); + }, + ), GoRoute( path: '/operations/form/:id', diff --git a/lib/core/routes/routes.dart b/lib/core/routes/routes.dart index 991823e..a34609f 100644 --- a/lib/core/routes/routes.dart +++ b/lib/core/routes/routes.dart @@ -19,6 +19,7 @@ class Routes { static const String operationForm = 'operation-form'; static const String uploadSuccess = 'upload-success'; static const String customerForm = 'customer-form'; + static const String customerDetails = 'customer-details'; static const String upload = 'upload'; static const String ticketWorkspace = 'ticket-workspace'; } diff --git a/lib/features/customers/blocs/customer_form_cubit.dart b/lib/features/customers/blocs/customer_form_cubit.dart new file mode 100644 index 0000000..cc4c6d2 --- /dev/null +++ b/lib/features/customers/blocs/customer_form_cubit.dart @@ -0,0 +1,106 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flux/core/blocs/session/session_cubit.dart'; +import 'package:flux/features/customers/data/customer_repository.dart'; +import 'package:flux/features/customers/models/customer_model.dart'; +import 'package:get_it/get_it.dart'; + +part 'customer_form_state.dart'; + +class CustomerFormCubit extends Cubit { + final CustomerRepository _repository = GetIt.I(); + final SessionCubit _sessionCubit = GetIt.I(); + + CustomerFormCubit({CustomerModel? existingCustomer}) + : super( + CustomerFormState(customer: existingCustomer ?? CustomerModel.empty()), + ); + + Future initForm({ + CustomerModel? existingCustomer, + String? customerId, + }) async { + emit(state.copyWith(status: CustomerFormStatus.loading)); + + try { + if (existingCustomer != null) { + emit( + state.copyWith( + customer: existingCustomer, + status: CustomerFormStatus.ready, + ), + ); + } else if (customerId != null) { + final customer = await _repository.getCustomerById(customerId); + emit( + state.copyWith(customer: customer, status: CustomerFormStatus.ready), + ); + } else { + // Nuovo cliente, inizializziamo con valori vuoti + emit( + state.copyWith( + customer: CustomerModel.empty().copyWith( + companyId: _sessionCubit.state.company!.id!, + ), + status: CustomerFormStatus.ready, + ), + ); + } + } on Exception catch (e) { + emit( + state.copyWith( + status: CustomerFormStatus.failure, + errorMessage: e.toString(), + ), + ); + } + } + + void updateDoNotDisturb(bool value) { + emit( + state.copyWith(customer: state.customer.copyWith(doNotDisturb: value)), + ); + } + + void updateFields({ + String? name, + String? phoneNumber, + String? email, + String? note, + bool? doNotDisturb, + }) { + emit( + state.copyWith( + customer: state.customer.copyWith( + name: name ?? state.customer.name, + phoneNumber: phoneNumber ?? state.customer.phoneNumber, + email: email ?? state.customer.email, + note: note ?? state.customer.note, + doNotDisturb: doNotDisturb ?? state.customer.doNotDisturb, + ), + ), + ); + } + + Future saveCustomer() async { + emit(state.copyWith(status: CustomerFormStatus.saving)); + + try { + if (state.customer.id != null) { + // Aggiorna cliente esistente + await _repository.updateCustomer(state.customer); + } else { + // Crea nuovo cliente + await _repository.insertCustomer(state.customer); + } + emit(state.copyWith(status: CustomerFormStatus.success)); + } on Exception catch (e) { + emit( + state.copyWith( + status: CustomerFormStatus.failure, + errorMessage: e.toString(), + ), + ); + } + } +} diff --git a/lib/features/customers/blocs/customer_form_state.dart b/lib/features/customers/blocs/customer_form_state.dart new file mode 100644 index 0000000..3c69efe --- /dev/null +++ b/lib/features/customers/blocs/customer_form_state.dart @@ -0,0 +1,30 @@ +part of 'customer_form_cubit.dart'; + +enum CustomerFormStatus { initial, loading, ready, saving, success, failure } + +class CustomerFormState extends Equatable { + final CustomerFormStatus status; + final CustomerModel customer; + final String? errorMessage; + + const CustomerFormState({ + this.status = CustomerFormStatus.initial, + required this.customer, + this.errorMessage, + }); + + CustomerFormState copyWith({ + CustomerFormStatus? status, + CustomerModel? customer, + String? errorMessage, + }) { + return CustomerFormState( + status: status ?? this.status, + customer: customer ?? this.customer, + errorMessage: errorMessage, + ); + } + + @override + List get props => [status, customer, errorMessage]; +} diff --git a/lib/features/customers/data/customer_repository.dart b/lib/features/customers/data/customer_repository.dart index 43c31ff..80b85a3 100644 --- a/lib/features/customers/data/customer_repository.dart +++ b/lib/features/customers/data/customer_repository.dart @@ -11,7 +11,7 @@ class CustomerRepository { final String companyId = GetIt.I.get().state.company!.id!; // Crea un nuovo cliente - Future saveCustomer(CustomerModel customer) async { + Future insertCustomer(CustomerModel customer) async { try { final response = await _supabase .from('customer') @@ -57,6 +57,23 @@ class CustomerRepository { } } + Future getCustomerById(String customerId) async { + try { + final response = await _supabase + .from('customer') + .select(''' + *, + attachment(*) + ''') + .eq('id', customerId) + .single(); + + return CustomerModel.fromMap(response); + } catch (e) { + throw '$e'; + } + } + // Ricerca clienti per nome o telefono (fondamentale per la UX) Future> searchCustomers( String companyId, diff --git a/lib/features/customers/models/customer_model.dart b/lib/features/customers/models/customer_model.dart index db92020..7f07a03 100644 --- a/lib/features/customers/models/customer_model.dart +++ b/lib/features/customers/models/customer_model.dart @@ -44,6 +44,15 @@ class CustomerModel extends Equatable { attachments, ]; + factory CustomerModel.empty() => CustomerModel( + name: '', + phoneNumber: '', + email: '', + note: '', + companyId: + '', // Dovrebbe essere sempre fornito, ma lasciamo vuoto per sicurezza + ); + CustomerModel copyWith({ String? id, DateTime? createdAt, diff --git a/lib/features/customers/ui/customer_form.dart b/lib/features/customers/ui/customer_form.dart index c097b16..7ee285d 100644 --- a/lib/features/customers/ui/customer_form.dart +++ b/lib/features/customers/ui/customer_form.dart @@ -1,16 +1,14 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/widgets/flux_text_field.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 { - final CustomerModel? customer; // Se presente, siamo in modalità "Modifica" - final Function(CustomerModel customer) onSave; + final CustomerModel? customer; + final String? customerId; - const CustomerForm({ - super.key, - this.customer, // Opzionale - required this.onSave, - }); + const CustomerForm({super.key, this.customer, this.customerId}); @override State createState() => _CustomerFormState(); @@ -20,25 +18,19 @@ class _CustomerFormState extends State { final _formKey = GlobalKey(); // Controller inizializzati con i dati del cliente (se presenti) - late final TextEditingController _nomeController; - late final TextEditingController _telefonoController; - late final TextEditingController _emailController; - late final TextEditingController _noteController; - late bool _nonDisturbare; + final TextEditingController _nomeController = TextEditingController(); + final TextEditingController _telefonoController = TextEditingController(); + final TextEditingController _emailController = TextEditingController(); + final TextEditingController _noteController = TextEditingController(); + bool _isInitialized = false; @override void initState() { super.initState(); - // Se widget.customer è null, i campi saranno vuoti - _nomeController = TextEditingController(text: widget.customer?.name ?? ''); - _telefonoController = TextEditingController( - text: widget.customer?.phoneNumber ?? '', + context.read().initForm( + customerId: widget.customerId, + existingCustomer: widget.customer, ); - _emailController = TextEditingController( - text: widget.customer?.email ?? '', - ); - _noteController = TextEditingController(text: widget.customer?.note ?? ''); - _nonDisturbare = widget.customer?.doNotDisturb ?? false; } @override @@ -50,89 +42,131 @@ class _CustomerFormState extends State { super.dispose(); } - void _submit() { - if (_formKey.currentState!.validate()) { - // Creiamo un nuovo modello partendo da quello esistente (se c'è) - // o creandone uno da zero, preservando l'ID in caso di modifica. - final updatedCustomer = - widget.customer?.copyWith( - name: _nomeController.text.trim(), - phoneNumber: _telefonoController.text.trim(), - email: _emailController.text.trim(), - note: _noteController.text.trim(), - doNotDisturb: _nonDisturbare, - ) ?? - CustomerModel( - // Caso nuovo cliente - name: _nomeController.text.trim(), - phoneNumber: _telefonoController.text.trim(), - email: _emailController.text.trim(), - note: _noteController.text.trim(), - doNotDisturb: _nonDisturbare, - companyId: '', // Verrà iniettato dal Bloc o dal chiamante - ); + void _syncTextControllers(CustomerModel customer) { + if (_nomeController.text.isEmpty) { + _nomeController.text = customer.name; + } + if (_telefonoController.text.isEmpty) { + _telefonoController.text = customer.phoneNumber; + } + if (_emailController.text.isEmpty) { + _emailController.text = customer.email; + } + if (_noteController.text.isEmpty) { + _noteController.text = customer.note; + } + _isInitialized = true; + } - widget.onSave(updatedCustomer); + void _flushControllersToCubit() { + context.read().updateFields( + name: _nomeController.text.trim(), + phoneNumber: _telefonoController.text.trim(), + email: _emailController.text.trim(), + note: _noteController.text.trim(), + ); + } + + void _saveCustomer() { + if (_formKey.currentState!.validate()) { + _flushControllersToCubit(); + context.read().saveCustomer(); } } @override Widget build(BuildContext context) { - return Form( - key: _formKey, - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - widget.customer == null ? 'Nuovo Cliente' : 'Modifica Cliente', - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + return BlocConsumer( + listenWhen: (previous, current) => previous.status != current.status, + listener: (context, state) { + if (state.status == CustomerFormStatus.ready && !_isInitialized) { + _syncTextControllers(state.customer); + } + if (state.status == CustomerFormStatus.success) { + Navigator.of(context).pop(state.customer); + } else if (state.status == CustomerFormStatus.failure) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(state.errorMessage ?? 'Errore sconosciuto')), + ); + } + }, + builder: (context, state) { + return Scaffold( + appBar: AppBar( + title: Text( + state.customer.id == null + ? 'Nuovo Cliente' + : 'Modifica ${state.customer.name}', ), - const SizedBox(height: 20), - FluxTextField( - label: 'Nome Completo', - autoFocus: true, - icon: Icons.person_outline, - controller: _nomeController, - ), - const SizedBox(height: 16), - FluxTextField( - label: 'Telefono', - icon: Icons.phone_android_outlined, - controller: _telefonoController, - keyboardType: TextInputType.phone, - ), - const SizedBox(height: 16), - FluxTextField( - label: 'Email', - icon: Icons.alternate_email_outlined, - controller: _emailController, - ), - const SizedBox(height: 16), - FluxTextField( - label: 'Note', - icon: Icons.notes_outlined, - controller: _noteController, - minLines: 3, - ), - const SizedBox(height: 8), - SwitchListTile( - title: const Text('Non disturbare'), - value: _nonDisturbare, - onChanged: (v) => setState(() => _nonDisturbare = v), - ), - const SizedBox(height: 24), - SizedBox( - width: double.infinity, - height: 50, - child: ElevatedButton( - onPressed: _submit, - child: Text(widget.customer == null ? 'SALVA' : 'AGGIORNA'), + actions: [], + ), + body: Padding( + padding: const EdgeInsets.all(8.0), + child: Form( + key: _formKey, + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + FluxTextField( + label: 'Nome Completo', + autoFocus: true, + icon: Icons.person_outline, + controller: _nomeController, + keyboardType: TextInputType.name, + ), + const SizedBox(height: 16), + FluxTextField( + label: 'Telefono', + icon: Icons.phone_android_outlined, + controller: _telefonoController, + keyboardType: TextInputType.phone, + ), + const SizedBox(height: 16), + FluxTextField( + label: 'Email', + icon: Icons.alternate_email_outlined, + controller: _emailController, + keyboardType: TextInputType.emailAddress, + ), + const SizedBox(height: 16), + FluxTextField( + label: 'Note', + icon: Icons.notes_outlined, + controller: _noteController, + minLines: 3, + ), + const SizedBox(height: 8), + SwitchListTile( + title: const Text('Non disturbare'), + value: state.customer.doNotDisturb, + onChanged: (v) => context + .read() + .updateDoNotDisturb(v), + ), + const SizedBox(height: 24), + SizedBox( + width: double.infinity, + height: 50, + child: ElevatedButton( + onPressed: _saveCustomer, + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).primaryColor, + foregroundColor: Colors.white, + ), + + child: Text( + widget.customer == null ? 'SALVA' : 'AGGIORNA', + ), + ), + ), + ], + ), ), ), - ], - ), - ), + ), + ); + }, ); } } diff --git a/lib/features/customers/ui/customers_content.dart b/lib/features/customers/ui/customers_list_screen.dart similarity index 96% rename from lib/features/customers/ui/customers_content.dart rename to lib/features/customers/ui/customers_list_screen.dart index 36c5c17..7c3a46d 100644 --- a/lib/features/customers/ui/customers_content.dart +++ b/lib/features/customers/ui/customers_list_screen.dart @@ -8,14 +8,14 @@ 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 CustomersContent extends StatefulWidget { - const CustomersContent({super.key}); +class CustomersListScreen extends StatefulWidget { + const CustomersListScreen({super.key}); @override - State createState() => _CustomersContentState(); + State createState() => _CustomersListScreenState(); } -class _CustomersContentState extends State { +class _CustomersListScreenState extends State { final TextEditingController _searchController = TextEditingController(); @override @@ -111,7 +111,7 @@ class _CustomersContentState extends State { return _CustomerTile( customer: customer, onTap: () => context.pushNamed( - Routes.customerForm, + Routes.customerDetails, pathParameters: {'id': customer.id!}, extra: customer, ), diff --git a/lib/features/customers/utils/customer_utils.dart b/lib/features/customers/utils/customer_utils.dart new file mode 100644 index 0000000..e69de29