diff --git a/lib/features/customers/blocs/customer_bloc.dart b/lib/features/customers/blocs/customer_bloc.dart index 8d70829..72a48ad 100644 --- a/lib/features/customers/blocs/customer_bloc.dart +++ b/lib/features/customers/blocs/customer_bloc.dart @@ -14,6 +14,7 @@ class CustomerBloc extends Bloc { on(_onLoadCustomers); on(_onCreateCustomer); on(_onSearchCustomers); + on(_onUpdateCustomer); } Future _onLoadCustomers( @@ -66,6 +67,40 @@ class CustomerBloc extends Bloc { } } + Future _onUpdateCustomer( + UpdateCustomerRequested event, + Emitter emit, + ) async { + emit(state.copyWith(status: CustomerStatus.loading)); + try { + // Qui dovresti aggiungere un metodo updateCustomer nel Repository + // Simile al create ma usando .update().eq('id', customer.id) + final updatedCustomer = await _repository.updateCustomer(event.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: CustomerStatus.success, + customers: updatedList, + lastCreatedCustomer: updatedCustomer, + ), + ); + } catch (e) { + emit( + state.copyWith( + status: CustomerStatus.failure, + errorMessage: e.toString(), + ), + ); + } + } + Future _onSearchCustomers( SearchCustomersRequested event, Emitter emit, diff --git a/lib/features/customers/blocs/customer_events.dart b/lib/features/customers/blocs/customer_events.dart index c031079..baec73f 100644 --- a/lib/features/customers/blocs/customer_events.dart +++ b/lib/features/customers/blocs/customer_events.dart @@ -24,3 +24,11 @@ class SearchCustomersRequested extends CustomerEvent { final String query; const SearchCustomersRequested(this.companyId, this.query); } + +class UpdateCustomerRequested extends CustomerEvent { + final CustomerModel customer; + const UpdateCustomerRequested(this.customer); + + @override + List get props => [customer]; +} diff --git a/lib/features/customers/data/customer_repository.dart b/lib/features/customers/data/customer_repository.dart index f672751..9b83948 100644 --- a/lib/features/customers/data/customer_repository.dart +++ b/lib/features/customers/data/customer_repository.dart @@ -19,6 +19,20 @@ class CustomerRepository { } } + Future updateCustomer(CustomerModel customer) async { + try { + final response = await _client + .from('customer') + .update(customer.toJson()) + .eq('id', customer.id!) + .select() + .single(); + return CustomerModel.fromJson(response); + } catch (e) { + throw 'Errore durante la modifica del cliente: $e'; + } + } + // Recupera tutti i clienti dell'azienda Future> getCustomers(String companyId) async { try { diff --git a/lib/features/customers/ui/customer_form.dart b/lib/features/customers/ui/customer_form.dart new file mode 100644 index 0000000..fedcc08 --- /dev/null +++ b/lib/features/customers/ui/customer_form.dart @@ -0,0 +1,136 @@ +import 'package:flutter/material.dart'; +import 'package:flux/core/widgets/flux_text_field.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; + + const CustomerForm({ + super.key, + this.customer, // Opzionale + required this.onSave, + }); + + @override + State createState() => _CustomerFormState(); +} + +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; + + @override + void initState() { + super.initState(); + // Se widget.customer è null, i campi saranno vuoti + _nomeController = TextEditingController(text: widget.customer?.nome ?? ''); + _telefonoController = TextEditingController( + text: widget.customer?.telefono ?? '', + ); + _emailController = TextEditingController( + text: widget.customer?.email ?? '', + ); + _noteController = TextEditingController(text: widget.customer?.note ?? ''); + _nonDisturbare = widget.customer?.nonDisturbare ?? false; + } + + @override + void dispose() { + _nomeController.dispose(); + _telefonoController.dispose(); + _emailController.dispose(); + _noteController.dispose(); + 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( + nome: _nomeController.text.trim(), + telefono: _telefonoController.text.trim(), + email: _emailController.text.trim(), + note: _noteController.text.trim(), + nonDisturbare: _nonDisturbare, + ) ?? + CustomerModel( + // Caso nuovo cliente + nome: _nomeController.text.trim(), + telefono: _telefonoController.text.trim(), + email: _emailController.text.trim(), + note: _noteController.text.trim(), + nonDisturbare: _nonDisturbare, + companyId: '', // Verrà iniettato dal Bloc o dal chiamante + ); + + widget.onSave(updatedCustomer); + } + } + + @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), + ), + const SizedBox(height: 20), + FluxTextField( + label: 'Nome Completo', + 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, + ), + 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'), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/customers/ui/customers_content.dart b/lib/features/customers/ui/customers_content.dart new file mode 100644 index 0000000..b681a9d --- /dev/null +++ b/lib/features/customers/ui/customers_content.dart @@ -0,0 +1,232 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flux/core/blocs/session/session_bloc.dart'; +import 'package:flux/core/theme/theme.dart'; +import 'package:flux/features/customers/blocs/customer_bloc.dart'; +import 'package:flux/features/customers/models/customer_model.dart'; +import 'package:flux/features/customers/ui/customer_form.dart'; + +class CustomersContent extends StatefulWidget { + const CustomersContent({super.key}); + + @override + State createState() => _CustomersContentState(); +} + +class _CustomersContentState extends State { + final TextEditingController _searchController = TextEditingController(); + + @override + void initState() { + super.initState(); + _loadInitialCustomers(); + } + + void _loadInitialCustomers() { + final companyId = context.read().state.company?.id; + if (companyId != null) { + context.read().add(LoadCustomersRequested(companyId)); + } + } + + void _onSearch(String query) { + final companyId = context.read().state.company?.id; + if (companyId != null) { + context.read().add( + SearchCustomersRequested(companyId, query), + ); + } + } + + /// Funzione unica per gestire Creazione e Modifica + void _openCustomerForm({CustomerModel? customer}) { + showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + backgroundColor: context.background, + content: SizedBox( + width: 500, // Larghezza ottimale per desktop + child: CustomerForm( + customer: customer, + onSave: (customerFromForm) { + final session = context.read().state; + final companyId = session.company?.id; + + if (companyId == null) return; + + if (customer == null) { + // CASO NUOVO: Iniettiamo il companyId e inviamo l'evento create + context.read().add( + CreateCustomerRequested( + customerFromForm.copyWith(companyId: companyId), + ), + ); + } else { + // CASO MODIFICA: L'ID e il companyId sono già nel modello + context.read().add( + UpdateCustomerRequested(customerFromForm), + ); + } + Navigator.pop(dialogContext); + }, + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: context.background, + appBar: AppBar( + title: const Text( + 'Anagrafica Clienti', + style: TextStyle(fontWeight: FontWeight.bold), + ), + elevation: 0, + backgroundColor: context.background, + actions: [ + Padding( + padding: const EdgeInsets.only(right: 16), + child: ElevatedButton.icon( + onPressed: () => _openCustomerForm(), + icon: const Icon(Icons.person_add_alt_1_rounded, size: 20), + label: const Text('NUOVO'), + style: ElevatedButton.styleFrom( + backgroundColor: context.accent, + foregroundColor: Colors.white, + ), + ), + ), + ], + ), + body: Column( + children: [ + // BARRA DI RICERCA + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + child: TextField( + controller: _searchController, + onChanged: _onSearch, + decoration: InputDecoration( + hintText: 'Cerca cliente per nome o telefono...', + prefixIcon: const Icon(Icons.search), + filled: true, + fillColor: context.accent.withValues(alpha: 0.05), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide.none, + ), + ), + ), + ), + + // LISTA CLIENTI + Expanded( + child: BlocBuilder( + builder: (context, state) { + if (state.status == CustomerStatus.loading && + state.customers.isEmpty) { + return const Center(child: CircularProgressIndicator()); + } + + if (state.customers.isEmpty) { + return _buildEmptyState(context); + } + + return ListView.separated( + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 8, + ), + itemCount: state.customers.length, + separatorBuilder: (context, index) => + const SizedBox(height: 10), + itemBuilder: (context, index) { + final customer = state.customers[index]; + return _CustomerTile( + customer: customer, + onTap: () => _openCustomerForm(customer: customer), + ); + }, + ); + }, + ), + ), + ], + ), + ); + } + + Widget _buildEmptyState(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.people_outline_rounded, + size: 64, + color: context.secondaryText.withValues(alpha: 0.5), + ), + const SizedBox(height: 16), + Text( + 'Nessun cliente in lista', + style: TextStyle(color: context.secondaryText), + ), + ], + ), + ); + } +} + +class _CustomerTile extends StatelessWidget { + final CustomerModel customer; + final VoidCallback onTap; + + const _CustomerTile({required this.customer, required this.onTap}); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: context.background, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: context.accent.withValues(alpha: 0.1)), + ), + child: ListTile( + onTap: onTap, + contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), + leading: CircleAvatar( + radius: 24, + backgroundColor: context.accent.withValues(alpha: 0.1), + child: Text( + customer.nome.isNotEmpty ? customer.nome[0].toUpperCase() : '?', + style: TextStyle( + color: context.accent, + fontWeight: FontWeight.bold, + ), + ), + ), + title: Text( + customer.nome, + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + ), + subtitle: Padding( + padding: const EdgeInsets.only(top: 4), + child: Row( + children: [ + Icon(Icons.phone_android, size: 14, color: context.secondaryText), + const SizedBox(width: 4), + Text( + customer.telefono, + style: TextStyle(color: context.secondaryText), + ), + ], + ), + ), + trailing: Icon(Icons.edit_note_rounded, color: context.accent), + ), + ); + } +} diff --git a/lib/features/home_and_dashboard/ui/home_screen.dart b/lib/features/home_and_dashboard/ui/home_screen.dart index bbae7c1..068f300 100644 --- a/lib/features/home_and_dashboard/ui/home_screen.dart +++ b/lib/features/home_and_dashboard/ui/home_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/blocs/session/session_bloc.dart'; import 'package:flux/core/theme/theme.dart'; +import 'package:flux/features/customers/ui/customers_content.dart'; import 'dashboard_content.dart'; // Importiamo il contenuto della dashboard class HomeScreen extends StatefulWidget { @@ -120,7 +121,7 @@ class _HomeScreenState extends State { case 0: return DashboardContent(isLargeScreen: isLargeScreen); case 1: - return const Center(child: Text('Pagina Clienti (Coming Soon)')); + return const CustomersContent(); case 2: return const Center(child: Text('Pagina Operazioni (Coming Soon)')); default: