diff --git a/lib/features/attachments/models/attachment_model.dart b/lib/features/attachments/models/attachment_model.dart new file mode 100644 index 0000000..35ad3b2 --- /dev/null +++ b/lib/features/attachments/models/attachment_model.dart @@ -0,0 +1,111 @@ +import 'dart:typed_data'; + +import 'package:equatable/equatable.dart'; + +class AttachmentModel extends Equatable { + final String? id; + final DateTime? createdAt; + final String? customerId; + final String? operationId; + final String name; + final String extension; + final String storagePath; + final int fileSize; + final Uint8List? localBytes; + final String companyId; + + const AttachmentModel({ + this.id, + this.createdAt, + this.customerId, + this.operationId, + required this.name, + required this.extension, + required this.storagePath, + required this.fileSize, + this.localBytes, + required this.companyId, + }); + + @override + List get props => [ + id, + createdAt, + customerId, + operationId, + name, + extension, + storagePath, + fileSize, + localBytes, + companyId, + ]; + + bool get isLocal => localBytes != null; + + bool get isPdf => extension.toLowerCase().replaceAll('.', '') == 'pdf'; + + 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]}"; + } + + AttachmentModel copyWith({ + String? id, + DateTime? createdAt, + String? customerId, + String? operationId, + String? name, + String? extension, + String? storagePath, + int? fileSize, + Uint8List? localBytes, + String? companyId, + }) => AttachmentModel( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + customerId: customerId ?? this.customerId, + operationId: operationId ?? this.operationId, + name: name ?? this.name, + extension: extension ?? this.extension, + storagePath: storagePath ?? this.storagePath, + fileSize: fileSize ?? this.fileSize, + localBytes: localBytes ?? this.localBytes, + companyId: companyId ?? this.companyId, + ); + + factory AttachmentModel.fromMap(Map map) { + return AttachmentModel( + id: map['id'] as String, + createdAt: map['created_at'] != null + ? DateTime.parse(map['created_at']) + : null, + customerId: map['customer_id'] as String?, + operationId: map['operation_id'] as String?, + name: map['name'] as String, + extension: map['extension'] as String, + storagePath: map['storage_path'] as String, + fileSize: map['file_size'] is int + ? map['file_size'] + : int.tryParse(map['file_size']?.toString() ?? '0') ?? 0, + companyId: map['company_id'] as String, + ); + } + + Map toMap() { + return { + if (id != null) 'id': id, + 'name': name, + 'extension': extension, + 'storage_path': storagePath, + 'customer_id': customerId, + 'operation_id': operationId, + 'file_size': fileSize, + 'company_id': companyId, + }; + } +} diff --git a/lib/features/customers/blocs/customer_cubit.dart b/lib/features/customers/blocs/customer_cubit.dart index d2d3f7c..8a2ff7f 100644 --- a/lib/features/customers/blocs/customer_cubit.dart +++ b/lib/features/customers/blocs/customer_cubit.dart @@ -135,8 +135,8 @@ class CustomerCubit extends Cubit { String? email, }) async { final newCustomer = CustomerModel( - nome: name, - telefono: phone ?? '', + name: name, + phoneNumber: phone ?? '', email: email ?? '', companyId: _sessionCubit.state.company!.id!, note: '', diff --git a/lib/features/customers/models/customer_model.dart b/lib/features/customers/models/customer_model.dart index d1fe55e..eb72c35 100644 --- a/lib/features/customers/models/customer_model.dart +++ b/lib/features/customers/models/customer_model.dart @@ -1,74 +1,74 @@ import 'package:equatable/equatable.dart'; import 'package:flux/core/utils/extensions.dart'; -import 'package:flux/features/customers/models/customer_file_model.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 files; + final List 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 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? files, + List? 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,34 +78,29 @@ 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)) - .toList() ?? - const [], ); } Map 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, }; diff --git a/lib/features/customers/ui/customer_detail_screen.dart b/lib/features/customers/ui/customer_detail_screen.dart index 53224b1..d06fb45 100644 --- a/lib/features/customers/ui/customer_detail_screen.dart +++ b/lib/features/customers/ui/customer_detail_screen.dart @@ -62,7 +62,7 @@ class _CustomerDetailScreenState extends State { 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 { 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 { : 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 { 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}', ), ); }, diff --git a/lib/features/customers/ui/customer_form.dart b/lib/features/customers/ui/customer_form.dart index 6de9695..c097b16 100644 --- a/lib/features/customers/ui/customer_form.dart +++ b/lib/features/customers/ui/customer_form.dart @@ -30,15 +30,15 @@ class _CustomerFormState extends State { 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 { // 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 ); diff --git a/lib/features/customers/ui/customer_search_sheet.dart b/lib/features/customers/ui/customer_search_sheet.dart index e93d980..d0a6bcc 100644 --- a/lib/features/customers/ui/customer_search_sheet.dart +++ b/lib/features/customers/ui/customer_search_sheet.dart @@ -103,7 +103,7 @@ class _CustomerSearchSheetState extends State { if (nuovoCliente != null) { operationsCubit.updateField( customerId: nuovoCliente.id, - customerDisplayName: nuovoCliente.nome, + customerDisplayName: nuovoCliente.name, ); setState(() { @@ -151,7 +151,7 @@ class _CustomerSearchSheetState extends State { 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(); + final displayName = customer.name.trim(); return ListTile( contentPadding: EdgeInsets.zero, diff --git a/lib/features/customers/ui/customers_content.dart b/lib/features/customers/ui/customers_content.dart index 46c54cd..e7b6b27 100644 --- a/lib/features/customers/ui/customers_content.dart +++ b/lib/features/customers/ui/customers_content.dart @@ -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) ...[ diff --git a/lib/features/operations/data/operations_repository.dart b/lib/features/operations/data/operations_repository.dart index 5d496d4..d4298ad 100644 --- a/lib/features/operations/data/operations_repository.dart +++ b/lib/features/operations/data/operations_repository.dart @@ -2,6 +2,7 @@ import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/utils/extensions.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:flux/features/operations/models/operation_file_model.dart'; @@ -21,11 +22,8 @@ class OperationsRepository { .from('operation') .select(''' *, - customer(nome), - energy_operation(*), - fin_operation(*), - entertainment_operation(*), - operation_file(*) + customer(name), + staff_member(name) ''') .eq('id', id) .single(); @@ -45,16 +43,13 @@ class OperationsRepository { DateTimeRange? dateRange, }) async { try { - // Nota: 'customer(name, surname)' serve per il display name nella card var query = _supabase .from('operation') .select(''' *, - customer(nome), - energy_operation(*), - fin_operation(*), - entertainment_operation(*), - operation_file(*) + customer(name), + staff_member(name), + attachments(*) ''') .eq('company_id', companyId); @@ -68,7 +63,7 @@ class OperationsRepository { if (searchTerm != null && searchTerm.isNotEmpty) { // Filtra sui campi della tabella principale O su quelli della tabella joinata query = query.or( - 'number.ilike.%$searchTerm%,note.ilike.%$searchTerm%,customer.nome.ilike.%$searchTerm%', + 'reference.ilike.%$searchTerm%,note.ilike.%$searchTerm%,customer.name.ilike.%$searchTerm%', ); } @@ -80,7 +75,7 @@ class OperationsRepository { .map((map) => OperationModel.fromMap(map)) .toList(); } catch (e) { - throw Exception('Errore nel caricamento servizi: $e'); + throw Exception('$e'); } } @@ -112,66 +107,9 @@ class OperationsRepository { final String newId = operationData['id']; - // 2. MODIFICA: Pulizia atomica dei figli - // Se stiamo modificando (id != null), resettiamo le tabelle collegate - if (operation.id != null) { - await Future.wait([ - _supabase.from('energy_operation').delete().eq('operation_id', newId), - _supabase.from('fin_operation').delete().eq('operation_id', newId), - _supabase - .from('entertainment_operation') - .delete() - .eq('operation_id', newId), - // Aggiungi qui eventuali altre tabelle pivot o file - ]); - } - - // 3. Inserimento dei moduli in parallelo per velocità - final List insertTasks = []; - - if (operation.energyOperations.isNotEmpty) { - insertTasks.add( - _supabase - .from('energy_operation') - .insert( - operation.energyOperations - .map((item) => item.copyWith(operationId: newId).toMap()) - .toList(), - ), - ); - } - - if (operation.finOperations.isNotEmpty) { - insertTasks.add( - _supabase - .from('fin_operation') - .insert( - operation.finOperations - .map((item) => item.copyWith(operationId: newId).toMap()) - .toList(), - ), - ); - } - - if (operation.entertainmentOperations.isNotEmpty) { - insertTasks.add( - _supabase - .from('entertainment_operation') - .insert( - operation.entertainmentOperations - .map((item) => item.copyWith(operationId: newId).toMap()) - .toList(), - ), - ); - } - - if (insertTasks.isNotEmpty) { - await Future.wait(insertTasks); - } - // 4. UPLOAD DEI FILE LOCALI (Nuovi) // Filtriamo solo i file che non hanno ancora un ID (quindi sono locali) - final localFilesToUpload = operation.files + final localFilesToUpload = operation.attachments .where((f) => f.id == null) .toList(); @@ -202,7 +140,7 @@ class OperationsRepository { ); // B. Inserimento riga nel DB relazionale - await _supabase.from('operation_file').insert(fileToSave.toMap()); + await _supabase.from('attachment').insert(fileToSave.toMap()); } uploadTasks.add(uploadAndLink()); @@ -219,10 +157,9 @@ class OperationsRepository { .from('operation') .select(''' *, - energy_operation(*), - fin_operation(*), - entertainment_operation(*), - operation_file(*) + staff_member(name), + customer(name), + attachments(*) ''') .eq('id', newId) .single(); @@ -230,7 +167,7 @@ class OperationsRepository { return OperationModel.fromMap(updatedOperationData); } catch (e) { // Qui potresti aggiungere una logica di "rollback manuale" se necessario - throw Exception('Errore durante il salvataggio corazzato: $e'); + throw Exception('$e'); } } @@ -239,7 +176,7 @@ class OperationsRepository { try { await _supabase.from('operation').delete().eq('id', id); } catch (e) { - throw Exception('Errore durante l\'eliminazione: $e'); + throw Exception('$e'); } } @@ -249,16 +186,17 @@ class OperationsRepository { // Cerchiamo i tipi più frequenti associati ai servizi di questa company // Nota: dobbiamo passare attraverso la tabella 'operation' per filtrare per company_id final response = await _supabase - .from('entertainment_operation') - .select('type, operation!inner(store!inner(company_id))') - .eq('operation.store.company_id', companyId) - .limit(100); // Prendiamo un campione + .from('operation') + .select('description') + .eq('company_id', companyId) + .eq('type', 'Entertainment') + .limit(50); // Prendiamo un campione // Logica rapida per contare le occorrenze e prendere i primi 5 final Map counts = {}; for (var item in (response as List)) { - final type = item['type'] as String; - counts[type] = (counts[type] ?? 0) + 1; + final description = item['description'] as String; + counts[description] = (counts[description] ?? 0) + 1; } var sortedKeys = counts.keys.toList() @@ -276,19 +214,19 @@ class OperationsRepository { } /// Ascolta in tempo reale i file caricati per una pratica - Stream> getOperationFilesStream(String operationId) { + Stream> getOperationFilesStream(String operationId) { return _supabase - .from('operation_file') + .from('attachment') .stream(primaryKey: ['id']) .eq('operation_id', operationId) .order('created_at', ascending: false) .map( (listOfMaps) => - listOfMaps.map((map) => OperationFileModel.fromMap(map)).toList(), + listOfMaps.map((map) => AttachmentModel.fromMap(map)).toList(), ); } - Future uploadAndRegisterOperationFile({ + Future uploadAndRegisterOperationFile({ required String operationId, required PlatformFile pickedFile, }) async { @@ -299,7 +237,8 @@ class OperationsRepository { final storagePath = '$companyId/operations/$operationId/${DateTime.now().millisecondsSinceEpoch}_$cleanFileName'; final int fileSize = pickedFile.size; - final fileToSave = OperationFileModel( + final fileToSave = AttachmentModel( + companyId: GetIt.I.get().state.company!.id!, operationId: operationId, name: cleanFileName.fileNameWithoutExtension(), extension: cleanFileName.fileExtension(), @@ -327,12 +266,12 @@ class OperationsRepository { } final response = await _supabase - .from('operation_file') + .from('attachment') .insert(fileToSave.toMap()) .select() .single(); - return OperationFileModel.fromMap(response); + return AttachmentModel.fromMap(response); } catch (e) { throw 'Errore durante l\'upload: $e'; } @@ -342,17 +281,13 @@ class OperationsRepository { required OperationFileModel file, required String customerId, }) async { - CustomerFileModel fileToCopy = CustomerFileModel( - customerId: customerId, - name: file.name, - storagePath: file.storagePath, - extension: file.extension, - fileSize: file.fileSize, - ); - await _customerRepository.saveFileReference(fileToCopy); + await _supabase + .from('attachment') + .update({'customer_id': customerId}) + .eq('id', file.id!); } - Future deleteOperationFiles(List files) async { + Future deleteOperationFiles(List files) async { if (files.isEmpty) return; // 1. Prepariamo le liste di ID e di Percorsi final List idsToDelete = files.map((f) => f.id!).toList(); @@ -360,18 +295,14 @@ class OperationsRepository { try { await _supabase - .from('operation_file') - .delete() + .from('attachment') + .update({'operation_id': null}) .inFilter('id', idsToDelete); await _supabase.storage.from('documents').remove(storagePaths); - - debugPrint("Eliminati con successo ${files.length} file."); } on PostgrestException catch (e) { - debugPrint("Errore DB: ${e.message}"); throw 'Errore database: ${e.message}'; } catch (e) { - debugPrint("Errore generico: $e"); throw 'Errore durante l\'eliminazione dei file: $e'; } } diff --git a/lib/features/operations/models/operation_model.dart b/lib/features/operations/models/operation_model.dart index b938d07..6f304a1 100644 --- a/lib/features/operations/models/operation_model.dart +++ b/lib/features/operations/models/operation_model.dart @@ -1,200 +1,200 @@ import 'package:equatable/equatable.dart'; -import 'package:flux/core/utils/extensions.dart'; -import 'package:flux/features/operations/models/energy_operation_model.dart'; -import 'package:flux/features/operations/models/entertainment_operation_model.dart'; -import 'package:flux/features/operations/models/fin_operation_model.dart'; -import 'package:flux/features/operations/models/operation_file_model.dart'; // <-- Aggiunto Import +import 'package:flux/features/attachments/models/attachment_model.dart'; + +enum OperationStatus { + ok('ok'), + waitingforaction('waiting_for_action'), + waitingforsupport('waiting_for_support'), + waitingfordeployment('waiting_for_deployment'), + ko('ko'), + draft('draft'), + canceled('canceled'); + + static OperationStatus fromString(String value) { + final normalizedValue = value.replaceAll('_', '').toLowerCase(); + return OperationStatus.values.firstWhere( + (e) => e.name.toLowerCase() == normalizedValue, + ); + } + + final String supabaseName; + + const OperationStatus(this.supabaseName); +} class OperationModel extends Equatable { final String? id; final DateTime? createdAt; - final String storeId; - final String? employeeId; - final String? customerId; - final String number; - final bool isBozza; + final String type; + final String? providerId; + final String? modelId; + final String? description; + final DateTime? expirationDate; final String note; - final bool resultOk; - final String? customerDisplayName; + final bool showInDashboard; + final String batchUuid; final String companyId; - - // Telefonia - final int al; - final int mnp; - final int nip; - final int unica; - final int telepass; - - // Moduli (Liste) - final List energyOperations; - final List finOperations; - final List entertainmentOperations; + final String storeId; + final int quantity; + final String? staffId; + final String staffDisplayName; + final String? lastCampaignId; + final OperationStatus status; + final String? customerId; + final String customerDisplayName; + final String reference; // ALLEGATI (Aggiunto) - final List files; + final List attachments; const OperationModel({ this.id, this.createdAt, - required this.storeId, - this.employeeId, - this.customerId, - required this.number, - this.isBozza = true, + this.type = '', + this.providerId, + this.modelId, + this.description, + this.expirationDate, this.note = '', - this.resultOk = true, - this.al = 0, - this.mnp = 0, - this.nip = 0, - this.unica = 0, - this.telepass = 0, - this.energyOperations = const [], - this.finOperations = const [], - this.entertainmentOperations = const [], - this.files = const [], // <-- Aggiunto default vuoto - this.customerDisplayName, + this.showInDashboard = true, + this.batchUuid = '', required this.companyId, + this.storeId = '', + this.quantity = 1, + this.staffId, + this.staffDisplayName = '', + this.lastCampaignId, + this.status = OperationStatus.draft, + this.customerId, + this.customerDisplayName = '', + this.reference = '', + this.attachments = const [], }); OperationModel copyWith({ String? id, DateTime? createdAt, - String? storeId, - String? employeeId, - String? customerId, - String? number, - bool? isBozza, + String? type, + String? providerId, + String? modelId, + String? description, + DateTime? expirationDate, String? note, - bool? resultOk, - int? al, - int? mnp, - int? nip, - int? unica, - int? telepass, - List? energyOperations, - List? finOperations, - List? entertainmentOperations, - List? files, // <-- Aggiunto - String? customerDisplayName, + bool? showInDashboard, + String? batchUuid, String? companyId, - }) { - return OperationModel( - id: id ?? this.id, - createdAt: createdAt ?? this.createdAt, - storeId: storeId ?? this.storeId, - employeeId: employeeId ?? this.employeeId, - customerId: customerId ?? this.customerId, - number: number ?? this.number, - isBozza: isBozza ?? this.isBozza, - note: note ?? this.note, - resultOk: resultOk ?? this.resultOk, - al: al ?? this.al, - mnp: mnp ?? this.mnp, - nip: nip ?? this.nip, - unica: unica ?? this.unica, - telepass: telepass ?? this.telepass, - energyOperations: energyOperations ?? this.energyOperations, - finOperations: finOperations ?? this.finOperations, - entertainmentOperations: - entertainmentOperations ?? this.entertainmentOperations, - files: files ?? this.files, // <-- Aggiunto - customerDisplayName: customerDisplayName ?? this.customerDisplayName, - companyId: companyId ?? this.companyId, - ); - } + String? storeId, + int? quantity, + String? staffId, + String? staffDisplayName, + String? lastCampaignId, + OperationStatus? status, + String? customerId, + String? customerDisplayName, + String? reference, + List? attachments, + }) => OperationModel( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + type: type ?? this.type, + providerId: providerId ?? this.providerId, + modelId: modelId ?? this.modelId, + description: description ?? this.description, + expirationDate: expirationDate ?? this.expirationDate, + note: note ?? this.note, + showInDashboard: showInDashboard ?? this.showInDashboard, + batchUuid: batchUuid ?? this.batchUuid, + companyId: companyId ?? this.companyId, + storeId: storeId ?? this.storeId, + quantity: quantity ?? this.quantity, + staffId: staffId ?? this.staffId, + staffDisplayName: staffDisplayName ?? this.staffDisplayName, + lastCampaignId: lastCampaignId ?? this.lastCampaignId, + status: status ?? this.status, + customerId: customerId ?? this.customerId, + customerDisplayName: customerDisplayName ?? this.customerDisplayName, + reference: reference ?? this.reference, + attachments: attachments ?? this.attachments, + ); @override List get props => [ id, createdAt, - storeId, - employeeId, - customerId, - number, - isBozza, + type, + providerId, + modelId, + description, + expirationDate, note, - resultOk, - al, - mnp, - nip, - unica, - telepass, - energyOperations, - finOperations, - entertainmentOperations, - files, // <-- Aggiunto - customerDisplayName, + showInDashboard, + batchUuid, companyId, + storeId, + quantity, + staffId, + staffDisplayName, + lastCampaignId, + status, + customerId, + customerDisplayName, + reference, + attachments, ]; + factory OperationModel.empty({required String companyId}) { + return OperationModel(id: null, createdAt: null, companyId: companyId); + } + factory OperationModel.fromMap(Map map) { return OperationModel( - id: map['id'].toString(), + id: map['id'], createdAt: map['created_at'] != null ? DateTime.parse(map['created_at']) - : DateTime.now(), - storeId: map['store_id'] ?? '', - employeeId: map['employee_id']?.toString(), - customerId: map['customer_id']?.toString(), - number: map['number']?.toString() ?? '', - isBozza: map['bozza'] ?? true, - note: map['note'] ?? '', - resultOk: map['result_ok'] ?? true, - al: map['al'] ?? 0, - mnp: map['mnp'] ?? 0, - nip: map['nip'] ?? 0, - unica: map['unica'] ?? 0, - telepass: map['telepass'] ?? 0, - - // Estrazione sicura liste collegate - energyOperations: - (map['energy_operation'] as List?) - ?.map((x) => EnergyOperationModel.fromMap(x)) - .toList() ?? - const [], - finOperations: - (map['fin_operation'] as List?) - ?.map((x) => FinOperationModel.fromMap(x)) - .toList() ?? - const [], - entertainmentOperations: - (map['entertainment_operation'] as List?) - ?.map((x) => EntertainmentOperationModel.fromMap(x)) - .toList() ?? - const [], - - // I FILE! (Assicurati che la foreign key su Supabase usi esattamente questo nome) - files: - (map['operation_file'] as List?) - ?.map((x) => OperationFileModel.fromMap(x)) - .toList() ?? - const [], - - // Display name del cliente con fallback - customerDisplayName: map['customer'] != null - ? "${map['customer']['nome'] ?? ''}".myFormat() - : "Cliente non assegnato", + : null, + type: map['type'] as String? ?? '', + providerId: map['provider_id'] as String? ?? '', + modelId: map['model_id'] as String? ?? '', + description: map['description'] as String? ?? '', + expirationDate: map['expiration_date'] != null + ? DateTime.parse(map['expiration_date']) + : null, + note: map['note'] as String? ?? '', + showInDashboard: map['show_in_dashboard'] as bool, + batchUuid: map['batch_uuid'] as String, companyId: map['company_id'] as String, + storeId: map['store_id'] as String? ?? '', + quantity: map['quantity'] is int + ? map['quantity'] + : int.tryParse(map['quantity']?.toString() ?? '0') ?? 0, + staffId: map['staff_id'] as String? ?? '', + lastCampaignId: map['last_campaign_id'] as String? ?? '', + status: OperationStatus.fromString(map['status']), + customerId: map['customer_id'] as String? ?? '', + reference: map['reference'] as String? ?? '', ); } Map toMap() { return { if (id != null) 'id': id, - 'store_id': storeId, - 'employee_id': employeeId, - 'customer_id': customerId, - 'number': number, - 'bozza': isBozza, + 'type': type, + 'provider_id': providerId, + 'model_id': modelId, + 'description': description, + if (expirationDate != null) + 'expiration_date': expirationDate!.toIso8601String(), 'note': note, - 'result_ok': resultOk, - 'al': al, - 'mnp': mnp, - 'nip': nip, - 'unica': unica, - 'telepass': telepass, + 'show_in_dashboard': showInDashboard, + 'batch_uuid': batchUuid, 'company_id': companyId, - // Le liste non le mettiamo qui perché vanno in tabelle diverse! + 'store_id': storeId, + 'quantity': quantity, + if (staffId != null) 'staff_id': staffId, + if (lastCampaignId != null) 'last_campaign_id': lastCampaignId, + 'status': status.supabaseName, + if (customerId != null) 'customer_id': customerId, + 'reference': reference, }; } }