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:
2026-05-04 15:36:42 +02:00
committed by brontomark
parent 9f57207a39
commit 94ad524bae
110 changed files with 5831 additions and 5306 deletions

View File

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

View File

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

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

View File

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

View File

@@ -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,
];
}

View File

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

View File

@@ -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,
];
}

View File

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

View File

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

View File

@@ -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
);

View File

@@ -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);
},
);
},
);
},
),
),
],
),
),
);
}
}

View File

@@ -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);
},

View File

@@ -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);