reworked operation (#12)
Reviewed-on: #12 Co-authored-by: Mark M2 Macbook <marco@catelli.it> Co-committed-by: Mark M2 Macbook <marco@catelli.it>
This commit is contained in:
@@ -4,8 +4,8 @@ import 'dart:io';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flux/features/attachments/models/attachment_model.dart';
|
||||
import 'package:flux/features/customers/data/customer_repository.dart';
|
||||
import 'package:flux/features/customers/models/customer_file_model.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
|
||||
part 'customer_files_events.dart';
|
||||
@@ -26,7 +26,7 @@ class CustomerFilesBloc extends Bloc<CustomerFilesEvent, CustomerFilesState> {
|
||||
LoadCustomerFilesEvent event,
|
||||
Emitter<CustomerFilesState> emit,
|
||||
) async {
|
||||
await emit.forEach<List<CustomerFileModel>>(
|
||||
await emit.forEach<List<AttachmentModel>>(
|
||||
_repository.getCustomerFilesStream(customerId),
|
||||
onData: (customerFiles) => CustomerFilesState(
|
||||
status: CustomerFilesStatus.success,
|
||||
@@ -128,7 +128,7 @@ class CustomerFilesBloc extends Bloc<CustomerFilesEvent, CustomerFilesState> {
|
||||
ToggleCustomerFileSelectionEvent event,
|
||||
Emitter<CustomerFilesState> emit,
|
||||
) {
|
||||
List<CustomerFileModel> selectedFiles = List.from(state.selectedFiles);
|
||||
List<AttachmentModel> selectedFiles = List.from(state.selectedFiles);
|
||||
if (selectedFiles.contains(event.file)) {
|
||||
selectedFiles.remove(event.file);
|
||||
} else {
|
||||
|
||||
@@ -25,6 +25,6 @@ class UploadMultipleCustomerFilesEvent extends CustomerFilesEvent {
|
||||
class DeleteCustomerFilesEvent extends CustomerFilesEvent {}
|
||||
|
||||
class ToggleCustomerFileSelectionEvent extends CustomerFilesEvent {
|
||||
final CustomerFileModel file;
|
||||
final AttachmentModel file;
|
||||
const ToggleCustomerFileSelectionEvent(this.file);
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ class CustomerFilesState extends Equatable {
|
||||
|
||||
final CustomerFilesStatus status;
|
||||
final String? error;
|
||||
final List<CustomerFileModel> customerFiles;
|
||||
final List<CustomerFileModel> selectedFiles;
|
||||
final List<AttachmentModel> customerFiles;
|
||||
final List<AttachmentModel> selectedFiles;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [status, error, customerFiles, selectedFiles];
|
||||
@@ -21,8 +21,8 @@ class CustomerFilesState extends Equatable {
|
||||
CustomerFilesState copyWith({
|
||||
CustomerFilesStatus? status,
|
||||
String? error,
|
||||
List<CustomerFileModel>? customerFiles,
|
||||
List<CustomerFileModel>? selectedFiles,
|
||||
List<AttachmentModel>? customerFiles,
|
||||
List<AttachmentModel>? selectedFiles,
|
||||
}) {
|
||||
return CustomerFilesState(
|
||||
status: status ?? this.status,
|
||||
|
||||
@@ -3,35 +3,34 @@ 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_file_model.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<CustomerState> {
|
||||
class CustomersCubit extends Cubit<CustomersState> {
|
||||
final CustomerRepository _repository = GetIt.I<CustomerRepository>();
|
||||
final SessionCubit _sessionCubit = GetIt.I<SessionCubit>();
|
||||
|
||||
// Variabile per gestire il debounce della ricerca
|
||||
Timer? _searchDebounce;
|
||||
|
||||
CustomerCubit() : super(const CustomerState());
|
||||
CustomersCubit() : super(const CustomersState());
|
||||
|
||||
// --- LETTURA ---
|
||||
Future<void> 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(),
|
||||
),
|
||||
);
|
||||
@@ -40,7 +39,7 @@ class CustomerCubit extends Cubit<CustomerState> {
|
||||
|
||||
// --- CREAZIONE ---
|
||||
Future<void> createCustomer(CustomerModel customer) async {
|
||||
emit(state.copyWith(status: CustomerStatus.loading));
|
||||
emit(state.copyWith(status: CustomersStatus.loading));
|
||||
try {
|
||||
final newCustomer = await _repository.saveCustomer(customer);
|
||||
|
||||
@@ -50,7 +49,7 @@ class CustomerCubit extends Cubit<CustomerState> {
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: CustomerStatus.success,
|
||||
status: CustomersStatus.success,
|
||||
customers: updatedList,
|
||||
lastCreatedCustomer: newCustomer,
|
||||
),
|
||||
@@ -58,7 +57,7 @@ class CustomerCubit extends Cubit<CustomerState> {
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: CustomerStatus.failure,
|
||||
status: CustomersStatus.failure,
|
||||
errorMessage: e.toString(),
|
||||
),
|
||||
);
|
||||
@@ -67,7 +66,7 @@ class CustomerCubit extends Cubit<CustomerState> {
|
||||
|
||||
// --- AGGIORNAMENTO ---
|
||||
Future<void> updateCustomer(CustomerModel customer) async {
|
||||
emit(state.copyWith(status: CustomerStatus.loading));
|
||||
emit(state.copyWith(status: CustomersStatus.loading));
|
||||
try {
|
||||
final updatedCustomer = await _repository.updateCustomer(customer);
|
||||
|
||||
@@ -80,7 +79,7 @@ class CustomerCubit extends Cubit<CustomerState> {
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: CustomerStatus.success,
|
||||
status: CustomersStatus.success,
|
||||
customers: updatedList,
|
||||
lastCreatedCustomer:
|
||||
updatedCustomer, // Utile se modifichi un cliente appena creato
|
||||
@@ -89,7 +88,7 @@ class CustomerCubit extends Cubit<CustomerState> {
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: CustomerStatus.failure,
|
||||
status: CustomersStatus.failure,
|
||||
errorMessage: e.toString(),
|
||||
),
|
||||
);
|
||||
@@ -116,12 +115,12 @@ class CustomerCubit extends Cubit<CustomerState> {
|
||||
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(),
|
||||
),
|
||||
);
|
||||
@@ -135,8 +134,8 @@ class CustomerCubit extends Cubit<CustomerState> {
|
||||
String? email,
|
||||
}) async {
|
||||
final newCustomer = CustomerModel(
|
||||
nome: name,
|
||||
telefono: phone ?? '',
|
||||
name: name,
|
||||
phoneNumber: phone ?? '',
|
||||
email: email ?? '',
|
||||
companyId: _sessionCubit.state.company!.id!,
|
||||
note: '',
|
||||
@@ -1,6 +1,6 @@
|
||||
part of 'customer_cubit.dart';
|
||||
part of 'customers_cubit.dart';
|
||||
|
||||
enum CustomerStatus {
|
||||
enum CustomersStatus {
|
||||
initial,
|
||||
loading,
|
||||
filesLoading,
|
||||
@@ -9,34 +9,30 @@ enum CustomerStatus {
|
||||
failure,
|
||||
}
|
||||
|
||||
class CustomerState extends Equatable {
|
||||
final CustomerStatus status;
|
||||
class CustomersState extends Equatable {
|
||||
final CustomersStatus status;
|
||||
final List<CustomerModel> customers;
|
||||
final CustomerModel? lastCreatedCustomer;
|
||||
final String? errorMessage;
|
||||
final List<CustomerFileModel> customerFiles;
|
||||
|
||||
const CustomerState({
|
||||
this.status = CustomerStatus.initial,
|
||||
const CustomersState({
|
||||
this.status = CustomersStatus.initial,
|
||||
this.customers = const [],
|
||||
this.lastCreatedCustomer,
|
||||
this.errorMessage,
|
||||
this.customerFiles = const [],
|
||||
});
|
||||
|
||||
CustomerState copyWith({
|
||||
CustomerStatus? status,
|
||||
CustomersState copyWith({
|
||||
CustomersStatus? status,
|
||||
List<CustomerModel>? customers,
|
||||
CustomerModel? lastCreatedCustomer,
|
||||
String? errorMessage,
|
||||
List<CustomerFileModel>? customerFiles,
|
||||
}) {
|
||||
return CustomerState(
|
||||
return CustomersState(
|
||||
status: status ?? this.status,
|
||||
customers: customers ?? this.customers,
|
||||
lastCreatedCustomer: lastCreatedCustomer ?? this.lastCreatedCustomer,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
customerFiles: customerFiles ?? this.customerFiles,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -46,6 +42,5 @@ class CustomerState extends Equatable {
|
||||
customers,
|
||||
lastCreatedCustomer,
|
||||
errorMessage,
|
||||
customerFiles,
|
||||
];
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flux/core/blocs/session/session_cubit.dart';
|
||||
import 'package:flux/core/utils/string_extensions.dart';
|
||||
import 'package:flux/features/customers/models/customer_file_model.dart';
|
||||
import 'package:flux/core/utils/extensions.dart';
|
||||
import 'package:flux/features/attachments/models/attachment_model.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import '../models/customer_model.dart';
|
||||
@@ -21,7 +20,7 @@ class CustomerRepository {
|
||||
.single();
|
||||
return CustomerModel.fromMap(response);
|
||||
} catch (e) {
|
||||
throw 'Errore durante il salvataggio del cliente: $e';
|
||||
throw '$e';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +34,7 @@ class CustomerRepository {
|
||||
.single();
|
||||
return CustomerModel.fromMap(response);
|
||||
} catch (e) {
|
||||
throw 'Errore durante la modifica del cliente: $e';
|
||||
throw '$e';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,15 +45,15 @@ class CustomerRepository {
|
||||
.from('customer')
|
||||
.select('''
|
||||
*,
|
||||
customer_file(*)
|
||||
attachment(*)
|
||||
''')
|
||||
.eq('company_id', companyId)
|
||||
.eq('is_active', true)
|
||||
.order('nome');
|
||||
.order('name');
|
||||
|
||||
return (response as List).map((c) => CustomerModel.fromMap(c)).toList();
|
||||
} catch (e) {
|
||||
throw 'Errore nel recupero clienti';
|
||||
throw '$e';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +67,7 @@ class CustomerRepository {
|
||||
.from('customer')
|
||||
.select()
|
||||
.eq('company_id', companyId)
|
||||
.or('nome.ilike.%$query%,telefono.ilike.%$query%')
|
||||
.or('name.ilike.%$query%,phone_number.ilike.%$query%')
|
||||
.limit(10);
|
||||
|
||||
return (response as List).map((c) => CustomerModel.fromMap(c)).toList();
|
||||
@@ -78,36 +77,34 @@ class CustomerRepository {
|
||||
}
|
||||
|
||||
/// Ascolta in tempo reale i file caricati per un cliente
|
||||
Stream<List<CustomerFileModel>> getCustomerFilesStream(String customerId) {
|
||||
Stream<List<AttachmentModel>> getCustomerFilesStream(String customerId) {
|
||||
return _supabase
|
||||
.from('customer_file')
|
||||
.from('attachment')
|
||||
.stream(primaryKey: ['id'])
|
||||
.eq('customer_id', customerId)
|
||||
.order('created_at', ascending: false)
|
||||
.map(
|
||||
(listOfMaps) =>
|
||||
listOfMaps.map((map) => CustomerFileModel.fromMap(map)).toList(),
|
||||
listOfMaps.map((map) => AttachmentModel.fromMap(map)).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Recupera i file di un cliente specifico
|
||||
Future<List<CustomerFileModel>> getCustomerFiles(String customerId) async {
|
||||
Future<List<AttachmentModel>> getCustomerFiles(String customerId) async {
|
||||
try {
|
||||
final response = await _supabase
|
||||
.from('customer_file')
|
||||
.from('attachment')
|
||||
.select()
|
||||
.eq('customer_id', customerId);
|
||||
|
||||
return (response as List)
|
||||
.map((f) => CustomerFileModel.fromMap(f))
|
||||
.toList();
|
||||
return (response as List).map((f) => AttachmentModel.fromMap(f)).toList();
|
||||
} catch (e) {
|
||||
throw 'Errore recupero file: $e';
|
||||
throw '$e';
|
||||
}
|
||||
}
|
||||
|
||||
/// Carica un file e salva il riferimento nel database
|
||||
Future<CustomerFileModel> uploadAndRegisterFile({
|
||||
Future<AttachmentModel> uploadAndRegisterFile({
|
||||
required String customerId,
|
||||
required PlatformFile pickedFile,
|
||||
}) async {
|
||||
@@ -118,7 +115,8 @@ class CustomerRepository {
|
||||
final storagePath =
|
||||
'$companyId/customers/${DateTime.now().millisecondsSinceEpoch}_$cleanFileName';
|
||||
final int fileSize = pickedFile.size;
|
||||
final fileToSave = CustomerFileModel(
|
||||
final fileToSave = AttachmentModel(
|
||||
companyId: companyId,
|
||||
customerId: customerId,
|
||||
name: cleanFileName.fileNameWithoutExtension(),
|
||||
extension: cleanFileName.fileExtension(),
|
||||
@@ -131,7 +129,7 @@ class CustomerRepository {
|
||||
try {
|
||||
// Usiamo bytes invece del path per massima compatibilità
|
||||
if (pickedFile.bytes == null && pickedFile.path == null) {
|
||||
throw 'Impossibile leggere il contenuto del file';
|
||||
throw 'File read error';
|
||||
}
|
||||
|
||||
// Se siamo su desktop/mobile abbiamo il path, su web abbiamo i bytes
|
||||
@@ -146,54 +144,51 @@ class CustomerRepository {
|
||||
}
|
||||
|
||||
final response = await _supabase
|
||||
.from('customer_file')
|
||||
.from('attachment')
|
||||
.insert(fileToSave.toMap())
|
||||
.select()
|
||||
.single();
|
||||
|
||||
return CustomerFileModel.fromMap(response);
|
||||
return AttachmentModel.fromMap(response);
|
||||
} catch (e) {
|
||||
throw 'Errore durante l\'upload: $e';
|
||||
throw '$e';
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> saveFileReference(CustomerFileModel file) async {
|
||||
await _supabase.from('customer_file').upsert(file.toMap());
|
||||
Future<void> saveFileReference(AttachmentModel file) async {
|
||||
await _supabase.from('attachment').upsert(file.toMap());
|
||||
}
|
||||
|
||||
/// Aggiorna la lista degli URL nel database
|
||||
Future<void> updateCustomerDocuments(int id, List<String> urls) async {
|
||||
await _supabase
|
||||
.from('customer')
|
||||
.update({'document_urls': urls})
|
||||
.eq('id', id);
|
||||
}
|
||||
|
||||
Future<void> deleteDocuments(List<CustomerFileModel> files) async {
|
||||
Future<void> deleteDocuments(List<AttachmentModel> files) async {
|
||||
if (files.isEmpty) return;
|
||||
|
||||
// 1. Prepariamo le liste di ID e di Percorsi
|
||||
final List<String> idsToDelete = files.map((f) => f.id!).toList();
|
||||
final List<String> storagePaths = files.map((f) => f.storagePath).toList();
|
||||
|
||||
final List<String> idsToDelete = [];
|
||||
final List<String> storagePathsToDelete = [];
|
||||
final List<String> idsToEdit = [];
|
||||
for (var file in files) {
|
||||
if (file.operationId == null) {
|
||||
idsToDelete.add(file.id!);
|
||||
storagePathsToDelete.add(file.storagePath!);
|
||||
} else {
|
||||
idsToEdit.add(file.id!);
|
||||
}
|
||||
}
|
||||
try {
|
||||
// 2. Cancellazione MASSIVA dal DB (una sola chiamata invece di un ciclo!)
|
||||
// .in_ dice: "cancella tutti i record il cui ID è contenuto in questa lista"
|
||||
await _supabase
|
||||
.from('customer_file')
|
||||
.delete()
|
||||
.inFilter('id', idsToDelete);
|
||||
|
||||
// 3. Cancellazione MASSIVA dallo Storage
|
||||
await _supabase.storage.from('documents').remove(storagePaths);
|
||||
|
||||
debugPrint("Eliminati con successo ${files.length} file.");
|
||||
if (idsToDelete.isNotEmpty) {
|
||||
await _supabase.from('attachment').delete().inFilter('id', idsToDelete);
|
||||
// 3. Cancellazione MASSIVA dallo Storage
|
||||
await _supabase.storage.from('documents').remove(storagePathsToDelete);
|
||||
}
|
||||
if (idsToEdit.isNotEmpty) {
|
||||
await _supabase
|
||||
.from('attachment')
|
||||
.update({'customer_id': null})
|
||||
.inFilter('id', idsToEdit);
|
||||
}
|
||||
} on PostgrestException catch (e) {
|
||||
debugPrint("Errore DB: ${e.message}");
|
||||
throw 'Errore database: ${e.message}';
|
||||
throw e.message;
|
||||
} catch (e) {
|
||||
debugPrint("Errore generico: $e");
|
||||
throw 'Errore durante l\'eliminazione dei file: $e';
|
||||
throw '$e';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class CustomerFileModel extends Equatable {
|
||||
final String? id;
|
||||
final String customerId; // Riferimento UUID
|
||||
final String name;
|
||||
final String storagePath;
|
||||
final String extension;
|
||||
final DateTime? createdAt;
|
||||
final int fileSize;
|
||||
|
||||
const CustomerFileModel({
|
||||
this.id,
|
||||
required this.customerId,
|
||||
required this.name,
|
||||
required this.storagePath,
|
||||
required this.extension,
|
||||
this.createdAt,
|
||||
required this.fileSize,
|
||||
});
|
||||
|
||||
// Trasforma i byte in qualcosa di leggibile (KB, MB, GB)
|
||||
String get sizeFormatted {
|
||||
if (fileSize <= 0) return "0 B";
|
||||
const suffixes = ["B", "KB", "MB", "GB", "TB"];
|
||||
var i = (fileSize.toString().length - 1) ~/ 3;
|
||||
if (i >= suffixes.length) i = suffixes.length - 1;
|
||||
double num = fileSize / (1 << (i * 10));
|
||||
return "${num.toStringAsFixed(i == 0 ? 0 : 1)} ${suffixes[i]}";
|
||||
}
|
||||
|
||||
bool get isPdf => extension.toLowerCase().replaceAll('.', '') == 'pdf';
|
||||
|
||||
CustomerFileModel copyWith({
|
||||
String? id,
|
||||
String? customerId,
|
||||
String? name,
|
||||
String? storagePath,
|
||||
String? extension,
|
||||
DateTime? createdAt,
|
||||
int? fileSize,
|
||||
}) {
|
||||
return CustomerFileModel(
|
||||
id: id ?? this.id,
|
||||
customerId: customerId ?? this.customerId,
|
||||
name: name ?? this.name,
|
||||
storagePath: storagePath ?? this.storagePath,
|
||||
extension: extension ?? this.extension,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
fileSize: fileSize ?? this.fileSize,
|
||||
);
|
||||
}
|
||||
|
||||
factory CustomerFileModel.fromMap(Map<String, dynamic> map) {
|
||||
return CustomerFileModel(
|
||||
id: map['id'] as String,
|
||||
customerId: map['customer_id'],
|
||||
name: map['name'],
|
||||
storagePath: map['storage_path'],
|
||||
extension: map['extension'] ?? '',
|
||||
createdAt: map['created_at'] != null
|
||||
? DateTime.parse(map['created_at'])
|
||||
: null,
|
||||
fileSize: map['file_size'] is int
|
||||
? map['file_size']
|
||||
: int.tryParse(map['file_size']?.toString() ?? '0') ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
if (id != null) 'id': id,
|
||||
'customer_id': customerId,
|
||||
'name': name,
|
||||
'storage_path': storagePath,
|
||||
'extension': extension,
|
||||
'file_size': fileSize,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
customerId,
|
||||
name,
|
||||
storagePath,
|
||||
extension,
|
||||
createdAt,
|
||||
fileSize,
|
||||
];
|
||||
}
|
||||
@@ -1,74 +1,74 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flux/core/utils/string_extensions.dart';
|
||||
import 'package:flux/features/customers/models/customer_file_model.dart';
|
||||
import 'package:flux/core/utils/extensions.dart';
|
||||
import 'package:flux/features/attachments/models/attachment_model.dart';
|
||||
|
||||
class CustomerModel extends Equatable {
|
||||
final String? id; // Bigint in SQL
|
||||
final DateTime? createdAt;
|
||||
final String nome;
|
||||
final String telefono;
|
||||
final String name;
|
||||
final String phoneNumber;
|
||||
final String email;
|
||||
final String note;
|
||||
final DateTime? dataUltimoContatto;
|
||||
final bool nonDisturbare;
|
||||
final DateTime? lastContactDate;
|
||||
final bool doNotDisturb;
|
||||
final String companyId; // UUID
|
||||
final bool isActive;
|
||||
final List<CustomerFileModel> files;
|
||||
final List<AttachmentModel> attachments;
|
||||
|
||||
const CustomerModel({
|
||||
this.id,
|
||||
this.createdAt,
|
||||
required this.nome,
|
||||
required this.telefono,
|
||||
required this.name,
|
||||
required this.phoneNumber,
|
||||
required this.email,
|
||||
required this.note,
|
||||
this.dataUltimoContatto,
|
||||
this.nonDisturbare = false,
|
||||
this.lastContactDate,
|
||||
this.doNotDisturb = false,
|
||||
required this.companyId,
|
||||
this.isActive = true,
|
||||
this.files = const [],
|
||||
this.attachments = const [],
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
createdAt,
|
||||
nome,
|
||||
telefono,
|
||||
name,
|
||||
phoneNumber,
|
||||
email,
|
||||
note,
|
||||
dataUltimoContatto,
|
||||
nonDisturbare,
|
||||
lastContactDate,
|
||||
doNotDisturb,
|
||||
companyId,
|
||||
isActive,
|
||||
files,
|
||||
attachments,
|
||||
];
|
||||
|
||||
CustomerModel copyWith({
|
||||
String? id,
|
||||
DateTime? createdAt,
|
||||
String? nome,
|
||||
String? telefono,
|
||||
String? name,
|
||||
String? phoneNumber,
|
||||
String? email,
|
||||
String? note,
|
||||
DateTime? dataUltimoContatto,
|
||||
bool? nonDisturbare,
|
||||
DateTime? lastContactDate,
|
||||
bool? doNotDisturb,
|
||||
String? companyId,
|
||||
bool? isActive,
|
||||
List<CustomerFileModel>? files,
|
||||
List<AttachmentModel>? attachments,
|
||||
}) {
|
||||
return CustomerModel(
|
||||
id: id ?? this.id,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
nome: nome ?? this.nome,
|
||||
telefono: telefono ?? this.telefono,
|
||||
name: name ?? this.name,
|
||||
phoneNumber: phoneNumber ?? this.phoneNumber,
|
||||
email: email ?? this.email,
|
||||
note: note ?? this.note,
|
||||
dataUltimoContatto: dataUltimoContatto ?? this.dataUltimoContatto,
|
||||
nonDisturbare: nonDisturbare ?? this.nonDisturbare,
|
||||
lastContactDate: lastContactDate ?? this.lastContactDate,
|
||||
doNotDisturb: doNotDisturb ?? this.doNotDisturb,
|
||||
companyId: companyId ?? this.companyId,
|
||||
isActive: isActive ?? this.isActive,
|
||||
files: files ?? this.files,
|
||||
attachments: attachments ?? this.attachments,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -78,19 +78,19 @@ class CustomerModel extends Equatable {
|
||||
createdAt: map['created_at'] != null
|
||||
? DateTime.parse(map['created_at'])
|
||||
: null,
|
||||
nome: (map['nome'] as String).myFormat(),
|
||||
telefono: map['telefono'],
|
||||
name: (map['name'] as String).myFormat(),
|
||||
phoneNumber: map['phone_number'],
|
||||
email: map['email'],
|
||||
note: map['note'] ?? '',
|
||||
dataUltimoContatto: map['data_ultimo_contatto'] != null
|
||||
? DateTime.parse(map['data_ultimo_contatto'])
|
||||
lastContactDate: map['last_contact_date'] != null
|
||||
? DateTime.parse(map['last_contact_date'])
|
||||
: null,
|
||||
nonDisturbare: map['non_disturbare'] ?? false,
|
||||
doNotDisturb: map['do_not_disturb'] ?? false,
|
||||
companyId: map['company_id'] as String,
|
||||
isActive: map['is_active'] ?? true,
|
||||
files:
|
||||
(map['customer_file'] as List?)
|
||||
?.map((x) => CustomerFileModel.fromMap(x))
|
||||
attachments:
|
||||
(map['attachment'] as List?)
|
||||
?.map((x) => AttachmentModel.fromMap(x))
|
||||
.toList() ??
|
||||
const [],
|
||||
);
|
||||
@@ -99,13 +99,13 @@ class CustomerModel extends Equatable {
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
if (id != null) 'id': id,
|
||||
'nome': nome.toLowerCase().trim(),
|
||||
'telefono': telefono,
|
||||
'name': name.toLowerCase().trim(),
|
||||
'phone_number': phoneNumber,
|
||||
'email': email.toLowerCase().trim(),
|
||||
'note': note,
|
||||
if (dataUltimoContatto != null)
|
||||
'data_ultimo_contatto': dataUltimoContatto!.toIso8601String(),
|
||||
'non_disturbare': nonDisturbare,
|
||||
if (lastContactDate != null)
|
||||
'last_contact_date': lastContactDate!.toIso8601String(),
|
||||
'do_not_disturb': doNotDisturb,
|
||||
'company_id': companyId,
|
||||
'is_active': isActive,
|
||||
};
|
||||
|
||||
@@ -6,9 +6,9 @@ import 'package:flux/core/theme/theme.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/customers/blocs/customer_files_bloc.dart';
|
||||
import 'package:flux/features/customers/models/customer_model.dart';
|
||||
import 'package:flux/features/customers/models/customer_file_model.dart';
|
||||
|
||||
class CustomerDetailScreen extends StatefulWidget {
|
||||
final CustomerModel customer;
|
||||
@@ -62,7 +62,7 @@ class _CustomerDetailScreenState extends State<CustomerDetailScreen> {
|
||||
backgroundColor: context.background,
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
widget.customer.nome,
|
||||
widget.customer.name,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
backgroundColor: context.background,
|
||||
@@ -103,7 +103,7 @@ class _CustomerDetailScreenState extends State<CustomerDetailScreen> {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_infoTile(Icons.phone_android, "Telefono", widget.customer.telefono),
|
||||
_infoTile(Icons.phone_android, "Telefono", widget.customer.phoneNumber),
|
||||
_infoTile(
|
||||
Icons.email_outlined,
|
||||
"Email",
|
||||
@@ -117,7 +117,7 @@ class _CustomerDetailScreenState extends State<CustomerDetailScreen> {
|
||||
: widget.customer.note,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
if (widget.customer.nonDisturbare)
|
||||
if (widget.customer.doNotDisturb)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
@@ -191,8 +191,8 @@ class _CustomerDetailScreenState extends State<CustomerDetailScreen> {
|
||||
context: context,
|
||||
builder: (context) => QrUploadDialog(
|
||||
deepLinkUrl:
|
||||
'fluxapp://customer/${widget.customer.id}/upload?name=${Uri.encodeComponent(widget.customer.nome)}',
|
||||
title: 'Scatta per ${widget.customer.nome}',
|
||||
'fluxapp://customer/${widget.customer.id}/upload?name=${Uri.encodeComponent(widget.customer.name)}',
|
||||
title: 'Scatta per ${widget.customer.name}',
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -262,12 +262,12 @@ class _CustomerDetailScreenState extends State<CustomerDetailScreen> {
|
||||
|
||||
void _showDeleteConfirmationDialog({
|
||||
required BuildContext context,
|
||||
required List<CustomerFileModel> files,
|
||||
required List<AttachmentModel> files,
|
||||
}) {}
|
||||
}
|
||||
|
||||
class _FileCard extends StatelessWidget {
|
||||
final CustomerFileModel file;
|
||||
final AttachmentModel file;
|
||||
final CustomerFilesState state;
|
||||
const _FileCard({required this.file, required this.state});
|
||||
|
||||
@@ -334,7 +334,7 @@ class _FileCard extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
void _handleDoubleClickOnFile(BuildContext context, CustomerFileModel file) {
|
||||
void _handleDoubleClickOnFile(BuildContext context, AttachmentModel file) {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
|
||||
@@ -30,15 +30,15 @@ class _CustomerFormState extends State<CustomerForm> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Se widget.customer è null, i campi saranno vuoti
|
||||
_nomeController = TextEditingController(text: widget.customer?.nome ?? '');
|
||||
_nomeController = TextEditingController(text: widget.customer?.name ?? '');
|
||||
_telefonoController = TextEditingController(
|
||||
text: widget.customer?.telefono ?? '',
|
||||
text: widget.customer?.phoneNumber ?? '',
|
||||
);
|
||||
_emailController = TextEditingController(
|
||||
text: widget.customer?.email ?? '',
|
||||
);
|
||||
_noteController = TextEditingController(text: widget.customer?.note ?? '');
|
||||
_nonDisturbare = widget.customer?.nonDisturbare ?? false;
|
||||
_nonDisturbare = widget.customer?.doNotDisturb ?? false;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -56,19 +56,19 @@ class _CustomerFormState extends State<CustomerForm> {
|
||||
// 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(),
|
||||
name: _nomeController.text.trim(),
|
||||
phoneNumber: _telefonoController.text.trim(),
|
||||
email: _emailController.text.trim(),
|
||||
note: _noteController.text.trim(),
|
||||
nonDisturbare: _nonDisturbare,
|
||||
doNotDisturb: _nonDisturbare,
|
||||
) ??
|
||||
CustomerModel(
|
||||
// Caso nuovo cliente
|
||||
nome: _nomeController.text.trim(),
|
||||
telefono: _telefonoController.text.trim(),
|
||||
name: _nomeController.text.trim(),
|
||||
phoneNumber: _telefonoController.text.trim(),
|
||||
email: _emailController.text.trim(),
|
||||
note: _noteController.text.trim(),
|
||||
nonDisturbare: _nonDisturbare,
|
||||
doNotDisturb: _nonDisturbare,
|
||||
companyId: '', // Verrà iniettato dal Bloc o dal chiamante
|
||||
);
|
||||
|
||||
|
||||
@@ -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/services/blocs/services_cubit.dart';
|
||||
|
||||
class CustomerSearchSheet extends StatefulWidget {
|
||||
const CustomerSearchSheet({super.key});
|
||||
|
||||
@override
|
||||
State<CustomerSearchSheet> createState() => _CustomerSearchSheetState();
|
||||
}
|
||||
|
||||
class _CustomerSearchSheetState extends State<CustomerSearchSheet> {
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
context.read<CustomerCubit>().loadCustomers();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onSearchChanged(String query) {
|
||||
context.read<CustomerCubit>().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 servicesCubit = context.read<ServicesCubit>();
|
||||
// Apriamo la dialog passando la query attuale
|
||||
final CustomerModel? nuovoCliente = await showDialog(
|
||||
context: context,
|
||||
builder: (context) => QuickCustomerDialog(
|
||||
initialQuery: _searchController.text,
|
||||
),
|
||||
);
|
||||
|
||||
if (nuovoCliente != null) {
|
||||
servicesCubit.updateField(
|
||||
customerId: nuovoCliente.id,
|
||||
customerDisplayName: nuovoCliente.nome,
|
||||
);
|
||||
|
||||
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<CustomerCubit, CustomerState>(
|
||||
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.nome.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<ServicesCubit>().updateField(
|
||||
customerId: customer.id,
|
||||
customerDisplayName: displayName,
|
||||
);
|
||||
|
||||
// Chiudiamo la modale
|
||||
Navigator.pop(context);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<CustomersContent> {
|
||||
void _loadInitialCustomers() {
|
||||
final companyId = context.read<SessionCubit>().state.company?.id;
|
||||
if (companyId != null) {
|
||||
context.read<CustomerCubit>().loadCustomers();
|
||||
context.read<CustomersCubit>().loadCustomers();
|
||||
}
|
||||
}
|
||||
|
||||
void _onSearch(String query) {
|
||||
final companyId = context.read<SessionCubit>().state.company?.id;
|
||||
if (companyId != null) {
|
||||
context.read<CustomerCubit>().searchCustomers(query);
|
||||
context.read<CustomersCubit>().searchCustomers(query);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,9 +86,9 @@ class _CustomersContentState extends State<CustomersContent> {
|
||||
|
||||
// LISTA CLIENTI
|
||||
Expanded(
|
||||
child: BlocBuilder<CustomerCubit, CustomerState>(
|
||||
child: BlocBuilder<CustomersCubit, CustomersState>(
|
||||
builder: (context, state) {
|
||||
if (state.status == CustomerStatus.loading &&
|
||||
if (state.status == CustomersStatus.loading &&
|
||||
state.customers.isEmpty) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
@@ -166,7 +166,7 @@ class _CustomerTile extends StatelessWidget {
|
||||
radius: 24,
|
||||
backgroundColor: context.accent.withValues(alpha: 0.1),
|
||||
child: Text(
|
||||
customer.nome.isNotEmpty ? customer.nome[0].toUpperCase() : '?',
|
||||
customer.name.isNotEmpty ? customer.name[0].toUpperCase() : '?',
|
||||
style: TextStyle(
|
||||
color: context.accent,
|
||||
fontWeight: FontWeight.bold,
|
||||
@@ -174,7 +174,7 @@ class _CustomerTile extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
customer.nome,
|
||||
customer.name,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||
),
|
||||
subtitle: Padding(
|
||||
@@ -184,7 +184,7 @@ class _CustomerTile extends StatelessWidget {
|
||||
Icon(Icons.phone_android, size: 14, color: context.secondaryText),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
customer.telefono,
|
||||
customer.phoneNumber,
|
||||
style: TextStyle(color: context.secondaryText),
|
||||
),
|
||||
if (customer.email.isNotEmpty) ...[
|
||||
@@ -196,11 +196,11 @@ class _CustomerTile extends StatelessWidget {
|
||||
style: TextStyle(color: context.secondaryText),
|
||||
),
|
||||
],
|
||||
if (customer.files.isNotEmpty) ...[
|
||||
if (customer.attachments.isNotEmpty) ...[
|
||||
Text(' - ', style: TextStyle(color: context.secondaryText)),
|
||||
Icon(Icons.attach_file, size: 14, color: context.accent),
|
||||
Text(
|
||||
'${customer.files.length} doc',
|
||||
'${customer.attachments.length} doc',
|
||||
style: TextStyle(
|
||||
color: context.accent,
|
||||
fontWeight: FontWeight.bold,
|
||||
@@ -242,12 +242,12 @@ void openCustomerForm({
|
||||
|
||||
if (customer == null) {
|
||||
// CASO NUOVO: Iniettiamo il companyId e inviamo l'evento create
|
||||
context.read<CustomerCubit>().createCustomer(
|
||||
context.read<CustomersCubit>().createCustomer(
|
||||
customerFromForm.copyWith(companyId: companyId),
|
||||
);
|
||||
} else {
|
||||
// CASO MODIFICA: L'ID e il companyId sono già nel modello
|
||||
context.read<CustomerCubit>().updateCustomer(customerFromForm);
|
||||
context.read<CustomersCubit>().updateCustomer(customerFromForm);
|
||||
}
|
||||
Navigator.pop(dialogContext);
|
||||
},
|
||||
|
||||
@@ -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<QuickCustomerDialog> {
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
// Chiamata al Cubit (aggiorna i parametri in base a come li hai definiti)
|
||||
final newCustomer = await context.read<CustomerCubit>().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<CustomersCubit>()
|
||||
.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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user