This commit is contained in:
2026-05-18 19:20:38 +02:00
parent 1ee4a3bf45
commit ecb161bc07
9 changed files with 317 additions and 105 deletions

View File

@@ -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',

View File

@@ -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';
}

View File

@@ -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<CustomerFormState> {
final CustomerRepository _repository = GetIt.I<CustomerRepository>();
final SessionCubit _sessionCubit = GetIt.I<SessionCubit>();
CustomerFormCubit({CustomerModel? existingCustomer})
: super(
CustomerFormState(customer: existingCustomer ?? CustomerModel.empty()),
);
Future<void> 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<void> 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(),
),
);
}
}
}

View File

@@ -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<Object?> get props => [status, customer, errorMessage];
}

View File

@@ -11,7 +11,7 @@ class CustomerRepository {
final String companyId = GetIt.I.get<SessionCubit>().state.company!.id!;
// Crea un nuovo cliente
Future<CustomerModel> saveCustomer(CustomerModel customer) async {
Future<CustomerModel> insertCustomer(CustomerModel customer) async {
try {
final response = await _supabase
.from('customer')
@@ -57,6 +57,23 @@ class CustomerRepository {
}
}
Future<CustomerModel> 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<List<CustomerModel>> searchCustomers(
String companyId,

View File

@@ -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,

View File

@@ -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<CustomerForm> createState() => _CustomerFormState();
@@ -20,25 +18,19 @@ class _CustomerFormState extends State<CustomerForm> {
final _formKey = GlobalKey<FormState>();
// 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<CustomerFormCubit>().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<CustomerForm> {
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<CustomerFormCubit>().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<CustomerFormCubit>().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<CustomerFormCubit, CustomerFormState>(
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<CustomerFormCubit>()
.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',
),
),
),
],
),
),
),
],
),
),
),
);
},
);
}
}

View File

@@ -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<CustomersContent> createState() => _CustomersContentState();
State<CustomersListScreen> createState() => _CustomersListScreenState();
}
class _CustomersContentState extends State<CustomersContent> {
class _CustomersListScreenState extends State<CustomersListScreen> {
final TextEditingController _searchController = TextEditingController();
@override
@@ -111,7 +111,7 @@ class _CustomersContentState extends State<CustomersContent> {
return _CustomerTile(
customer: customer,
onTap: () => context.pushNamed(
Routes.customerForm,
Routes.customerDetails,
pathParameters: {'id': customer.id!},
extra: customer,
),