diff --git a/lib/features/customers/blocs/customer_cubit.dart b/lib/features/customers/blocs/customer_cubit.dart index 8a2ff7f..fba9b4d 100644 --- a/lib/features/customers/blocs/customer_cubit.dart +++ b/lib/features/customers/blocs/customer_cubit.dart @@ -3,7 +3,6 @@ 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'; diff --git a/lib/features/customers/blocs/customer_files_bloc.dart b/lib/features/customers/blocs/customer_files_bloc.dart index 0fdffe1..c8662e0 100644 --- a/lib/features/customers/blocs/customer_files_bloc.dart +++ b/lib/features/customers/blocs/customer_files_bloc.dart @@ -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 { LoadCustomerFilesEvent event, Emitter emit, ) async { - await emit.forEach>( + await emit.forEach>( _repository.getCustomerFilesStream(customerId), onData: (customerFiles) => CustomerFilesState( status: CustomerFilesStatus.success, @@ -128,7 +128,7 @@ class CustomerFilesBloc extends Bloc { ToggleCustomerFileSelectionEvent event, Emitter emit, ) { - List selectedFiles = List.from(state.selectedFiles); + List selectedFiles = List.from(state.selectedFiles); if (selectedFiles.contains(event.file)) { selectedFiles.remove(event.file); } else { diff --git a/lib/features/customers/blocs/customer_files_events.dart b/lib/features/customers/blocs/customer_files_events.dart index ad235d1..b893ce8 100644 --- a/lib/features/customers/blocs/customer_files_events.dart +++ b/lib/features/customers/blocs/customer_files_events.dart @@ -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); } diff --git a/lib/features/customers/blocs/customer_files_state.dart b/lib/features/customers/blocs/customer_files_state.dart index 88fbe5b..bdb525d 100644 --- a/lib/features/customers/blocs/customer_files_state.dart +++ b/lib/features/customers/blocs/customer_files_state.dart @@ -12,8 +12,8 @@ class CustomerFilesState extends Equatable { final CustomerFilesStatus status; final String? error; - final List customerFiles; - final List selectedFiles; + final List customerFiles; + final List selectedFiles; @override List get props => [status, error, customerFiles, selectedFiles]; @@ -21,8 +21,8 @@ class CustomerFilesState extends Equatable { CustomerFilesState copyWith({ CustomerFilesStatus? status, String? error, - List? customerFiles, - List? selectedFiles, + List? customerFiles, + List? selectedFiles, }) { return CustomerFilesState( status: status ?? this.status, diff --git a/lib/features/customers/blocs/customer_state.dart b/lib/features/customers/blocs/customer_state.dart index 9e26c69..da5ffec 100644 --- a/lib/features/customers/blocs/customer_state.dart +++ b/lib/features/customers/blocs/customer_state.dart @@ -14,14 +14,12 @@ class CustomerState extends Equatable { final List customers; final CustomerModel? lastCreatedCustomer; final String? errorMessage; - final List customerFiles; const CustomerState({ this.status = CustomerStatus.initial, this.customers = const [], this.lastCreatedCustomer, this.errorMessage, - this.customerFiles = const [], }); CustomerState copyWith({ @@ -29,14 +27,12 @@ class CustomerState extends Equatable { List? customers, CustomerModel? lastCreatedCustomer, String? errorMessage, - List? customerFiles, }) { return CustomerState( 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, ]; } diff --git a/lib/features/customers/data/customer_repository.dart b/lib/features/customers/data/customer_repository.dart index 0831685..086db52 100644 --- a/lib/features/customers/data/customer_repository.dart +++ b/lib/features/customers/data/customer_repository.dart @@ -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/extensions.dart'; -import 'package:flux/features/customers/models/customer_file_model.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'; @@ -46,11 +45,11 @@ 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) { @@ -78,36 +77,34 @@ class CustomerRepository { } /// Ascolta in tempo reale i file caricati per un cliente - Stream> getCustomerFilesStream(String customerId) { + Stream> 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> getCustomerFiles(String customerId) async { + Future> 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 '$e'; } } /// Carica un file e salva il riferimento nel database - Future uploadAndRegisterFile({ + Future 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(), @@ -146,46 +144,47 @@ 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 '$e'; } } - Future saveFileReference(CustomerFileModel file) async { - await _supabase.from('customer_file').upsert(file.toMap()); + Future saveFileReference(AttachmentModel file) async { + await _supabase.from('attachment').upsert(file.toMap()); } - /// Aggiorna la lista degli URL nel database - Future updateCustomerDocuments(int id, List urls) async { - await _supabase - .from('customer') - .update({'document_urls': urls}) - .eq('id', id); - } - - Future deleteDocuments(List files) async { + Future deleteDocuments(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(); - final List storagePaths = files.map((f) => f.storagePath).toList(); - + final List idsToDelete = []; + final List storagePathsToDelete = []; + final List 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); + 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) { throw e.message; } catch (e) { diff --git a/lib/features/customers/models/customer_file_model.dart b/lib/features/customers/models/customer_file_model.dart deleted file mode 100644 index 00d1e92..0000000 --- a/lib/features/customers/models/customer_file_model.dart +++ /dev/null @@ -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 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 toMap() { - return { - if (id != null) 'id': id, - 'customer_id': customerId, - 'name': name, - 'storage_path': storagePath, - 'extension': extension, - 'file_size': fileSize, - }; - } - - @override - List get props => [ - id, - customerId, - name, - storagePath, - extension, - createdAt, - fileSize, - ]; -} diff --git a/lib/features/customers/models/customer_model.dart b/lib/features/customers/models/customer_model.dart index eb72c35..db92020 100644 --- a/lib/features/customers/models/customer_model.dart +++ b/lib/features/customers/models/customer_model.dart @@ -88,6 +88,11 @@ class CustomerModel extends Equatable { doNotDisturb: map['do_not_disturb'] ?? false, companyId: map['company_id'] as String, isActive: map['is_active'] ?? true, + attachments: + (map['attachment'] as List?) + ?.map((x) => AttachmentModel.fromMap(x)) + .toList() ?? + const [], ); } diff --git a/lib/features/customers/ui/customer_detail_screen.dart b/lib/features/customers/ui/customer_detail_screen.dart index d06fb45..b6e64da 100644 --- a/lib/features/customers/ui/customer_detail_screen.dart +++ b/lib/features/customers/ui/customer_detail_screen.dart @@ -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; @@ -262,12 +262,12 @@ class _CustomerDetailScreenState extends State { void _showDeleteConfirmationDialog({ required BuildContext context, - required List files, + required List 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, diff --git a/lib/features/customers/ui/customers_content.dart b/lib/features/customers/ui/customers_content.dart index e7b6b27..7021922 100644 --- a/lib/features/customers/ui/customers_content.dart +++ b/lib/features/customers/ui/customers_content.dart @@ -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, diff --git a/lib/features/home/latest_store_operations/ui/latest_store_operations_card.dart b/lib/features/home/latest_store_operations/ui/latest_store_operations_card.dart index 10123e0..1380b0a 100644 --- a/lib/features/home/latest_store_operations/ui/latest_store_operations_card.dart +++ b/lib/features/home/latest_store_operations/ui/latest_store_operations_card.dart @@ -156,7 +156,7 @@ class _LatestOperationsCardContent extends StatelessWidget { Expanded( flex: 5, child: Text( - operation.number, + operation.reference, style: TextStyle( fontWeight: FontWeight.w600, color: context.primaryText, diff --git a/lib/features/operations/blocs/operation_files_bloc.dart b/lib/features/operations/blocs/operation_files_bloc.dart index 6407d5a..34aca84 100644 --- a/lib/features/operations/blocs/operation_files_bloc.dart +++ b/lib/features/operations/blocs/operation_files_bloc.dart @@ -1,14 +1,13 @@ import 'dart:async'; -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/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/operations/data/operations_repository.dart'; -import 'package:flux/features/operations/models/operation_file_model.dart'; -import 'package:flux/features/operations/models/operation_model.dart'; import 'package:get_it/get_it.dart'; +import 'package:image_picker/image_picker.dart'; part 'operation_files_events.dart'; part 'operation_files_state.dart'; @@ -29,9 +28,10 @@ class OperationFilesBloc on(_onLoadOperationFiles); on(_onAddOperationFiles); on(_onUploadOperationFiles); - on(_onUploadMultipleOperationFiles); on(_onDeleteOperationFiles); on(_onToggleOperationFileSelection); + on(_onLinkFilesToCustomer); + // Se il BLoC nasce con un ID, accendiamo subito lo stream! if (operationId != null) { add(LoadOperationFilesEvent(operationId: operationId)); @@ -41,18 +41,53 @@ class OperationFilesBloc FutureOr _onOperationsaved( OperationsavedEvent event, Emitter emit, - ) { - // 1. Aggiorniamo l'ID nello stato - // 2. PIALLIAMO i file locali: ormai sono partiti per Supabase! - // Così la UI si pulisce all'istante e aspetta quelli remoti. + ) async { + // 1. Aggiorniamo l'ID e mettiamo in loading emit( state.copyWith( operationId: event.operationId, - localFiles: [], // <-- LA MAGIA ANTI-DUPLICATI + status: OperationFilesStatus.uploading, ), ); - // Lanciamo il caricamento + // 2. RECUPERO E UPLOAD DEI FILE "PARCHEGGIATI" (Pratica Nuova) + if (state.localFiles.isNotEmpty) { + try { + final List> uploadTasks = []; + + for (var file in state.localFiles) { + // Ricreiamo il PlatformFile dal nostro AttachmentModel + // così il repository lo accetta senza fare storie! + final fakePlatformFile = PlatformFile( + name: '${file.name}.${file.extension}', + size: file.fileSize, + bytes: file.localBytes, + ); + + uploadTasks.add( + _repository.uploadAndRegisterOperationFile( + operationId: event.operationId, // L'ID APPENA NATO! + pickedFile: fakePlatformFile, + ), + ); + } + + // Lanciamo tutti gli upload in parallelo + await Future.wait(uploadTasks); + } catch (e) { + emit( + state.copyWith( + status: OperationFilesStatus.failure, + error: "Errore upload post-salvataggio: $e", + ), + ); + return; // Ci fermiamo qui se esplode qualcosa + } + } + + // 3. FINE DEI GIOCHI! Svuotiamo i locali, passiamo a success e accendiamo lo Stream + emit(state.copyWith(localFiles: [], status: OperationFilesStatus.success)); + add(LoadOperationFilesEvent(operationId: event.operationId)); } @@ -60,17 +95,14 @@ class OperationFilesBloc LoadOperationFilesEvent event, Emitter emit, ) async { - // Usiamo l'ID dell'evento, e se non c'è usiamo quello dello stato final currentId = event.operationId ?? state.operationId; if (currentId != null) { emit(state.copyWith(status: OperationFilesStatus.loading)); await emit.forEach( - _repository.getOperationFilesStream( - currentId, - ), // <-- Usiamo l'ID corretto! - onData: (data) => state.copyWith( + _repository.getOperationFilesStream(currentId), + onData: (List data) => state.copyWith( status: OperationFilesStatus.success, remoteFiles: data, ), @@ -87,13 +119,15 @@ class OperationFilesBloc Emitter emit, ) async { final currentId = state.operationId; - // BIVIO 1: PRATICA NUOVA (Nessun ID) + + // BIVIO 1: PRATICA NUOVA (Nessun ID - salvataggio locale) if (currentId == null) { - // Mettiamo i file nel "parcheggio" locale dello State + final companyId = GetIt.I.get().state.company!.id!; final newLocalFiles = event.files.map((file) { - return OperationFileModel( + return AttachmentModel( id: null, - operationId: operationId ?? '', + companyId: companyId, + operationId: '', // Sarà riempito al salvataggio name: file.name.fileNameWithoutExtension(), extension: file.name.fileExtension(), storagePath: '', @@ -101,29 +135,29 @@ class OperationFilesBloc localBytes: file.bytes, ); }).toList(); - final List updatedLocalFiles = [ - ...state.localFiles, - ...newLocalFiles, - ]; + emit( state.copyWith( - localFiles: updatedLocalFiles, + localFiles: [...state.localFiles, ...newLocalFiles], status: OperationFilesStatus.success, ), ); return; } - // BIVIO 2: PRATICA ESISTENTE (Abbiamo l'ID) + // BIVIO 2: PRATICA ESISTENTE (Abbiamo l'ID - Upload immediato) emit(state.copyWith(status: OperationFilesStatus.uploading)); try { - // Logica identica a quella che abbiamo fatto per i clienti + final List> uploadTasks = []; for (var file in event.files) { - await _repository.uploadAndRegisterOperationFile( - operationId: operationId!, - pickedFile: file, + uploadTasks.add( + _repository.uploadAndRegisterOperationFile( + operationId: currentId, + pickedFile: file, + ), ); } + await Future.wait(uploadTasks); emit(state.copyWith(status: OperationFilesStatus.success)); } catch (e) { emit( @@ -139,21 +173,55 @@ class OperationFilesBloc UploadOperationFilesEvent event, Emitter emit, ) async { - if (event.pickedFiles == null && event.photos == null) return; - if (event.pickedFiles!.isEmpty && event.photos!.isEmpty) return; + if ((event.pickedFiles == null || event.pickedFiles!.isEmpty) && + (event.photos == null || event.photos!.isEmpty)) { + return; + } + + if (state.operationId == null) return; - // BIVIO 2: PRATICA ESISTENTE (Abbiamo l'ID emit(state.copyWith(status: OperationFilesStatus.uploading)); try { - // Logica identica a quella che abbiamo fatto per i clienti - if (event.pickedFiles != null && event.pickedFiles!.isNotEmpty) { + final List> uploadTasks = []; + + // 1. Gestione Documenti normali (PlatformFile) + if (event.pickedFiles != null) { for (var file in event.pickedFiles!) { - await _repository.uploadAndRegisterOperationFile( - operationId: state.operationId!, - pickedFile: file, + uploadTasks.add( + _repository.uploadAndRegisterOperationFile( + operationId: state.operationId!, + pickedFile: file, + ), ); } } + + // 2. Gestione Foto Fotocamera (XFile) + if (event.photos != null) { + for (var photo in event.photos!) { + // Leggiamo i byte asincronamente + final bytes = await photo.readAsBytes(); + final fileSize = await photo.length(); + + // Lo travestiamo da PlatformFile per passarlo al Repository! + final fakePlatformFile = PlatformFile( + name: photo.name, + size: fileSize, + bytes: bytes, + path: photo.path, + ); + + uploadTasks.add( + _repository.uploadAndRegisterOperationFile( + operationId: state.operationId!, + pickedFile: fakePlatformFile, + ), + ); + } + } + + // Esecuzione parallela di tutti i documenti e foto + await Future.wait(uploadTasks); emit(state.copyWith(status: OperationFilesStatus.success)); } catch (e) { emit( @@ -165,48 +233,6 @@ class OperationFilesBloc } } - FutureOr _onUploadMultipleOperationFiles( - UploadMultipleOperationFilesEvent event, - Emitter emit, - ) async { - if (event.files.isEmpty) { - emit( - state.copyWith( - status: OperationFilesStatus.failure, - error: "Nessun file selezionato", - ), - ); - return; - } - emit(state.copyWith(status: OperationFilesStatus.uploading, error: null)); - try { - // 2. Creiamo una lista di "Promesse" (Futures) per il repository - final List> uploadTasks = []; - for (var file in event.files) { - // Aggiungiamo il task alla lista, ma NON usiamo await qui dentro! - uploadTasks.add( - _repository.uploadAndRegisterOperationFile( - operationId: state.operationId!, - pickedFile: file, - ), - ); - } - // 3. ESECUZIONE PARALLELA! - // Aspettiamo che tutti i file siano caricati contemporaneamente. - await Future.wait(uploadTasks); - // 4. GRAN FINALE: Tutto caricato, emettiamo il success! - emit(state.copyWith(status: OperationFilesStatus.success)); - } catch (e) { - // Se anche un solo file fallisce, catturiamo l'errore - emit( - state.copyWith( - status: OperationFilesStatus.failure, - error: "Errore durante l'upload multiplo: $e", - ), - ); - } - } - FutureOr _onDeleteOperationFiles( DeleteOperationFilesEvent event, Emitter emit, @@ -231,7 +257,7 @@ class OperationFilesBloc ToggleOperationFileSelectionEvent event, Emitter emit, ) { - List selectedFiles = List.from(state.selectedFiles); + final selectedFiles = List.from(state.selectedFiles); if (selectedFiles.contains(event.file)) { selectedFiles.remove(event.file); } else { @@ -239,4 +265,62 @@ class OperationFilesBloc } emit(state.copyWith(selectedFiles: selectedFiles)); } + + FutureOr _onLinkFilesToCustomer( + LinkFilesToCustomerEvent event, + Emitter emit, + ) async { + if (state.selectedFiles.isEmpty) return; + + // BIVIO 1: PRATICA NUOVA (Modalità Locale) + if (state.operationId == null) { + // Mappiamo i file locali: se sono tra quelli selezionati, iniettiamo il customerId + final updatedLocalFiles = state.localFiles.map((file) { + if (state.selectedFiles.contains(file)) { + return file.copyWith(customerId: event.customerId); + } + return file; + }).toList(); + + emit( + state.copyWith( + localFiles: updatedLocalFiles, + selectedFiles: [], // Svuotiamo la selezione dopo averli associati + status: OperationFilesStatus.success, // o un toast di feedback + ), + ); + return; + } + + // BIVIO 2: PRATICA ESISTENTE (Modalità Remota su DB) + emit(state.copyWith(status: OperationFilesStatus.loading)); + try { + final List> linkTasks = []; + + for (var file in state.selectedFiles) { + linkTasks.add( + _repository.copyFileToCustomer( + file: file, + customerId: event.customerId, + ), + ); + } + + await Future.wait(linkTasks); + + // Svuotiamo la selezione. + // NON serve aggiornare la lista a mano, perché il DB si aggiorna + // e lo Stream di Supabase spingerà automaticamente in UI i file aggiornati! + emit( + state.copyWith(status: OperationFilesStatus.success, selectedFiles: []), + ); + } catch (e) { + emit( + state.copyWith( + status: OperationFilesStatus.failure, + error: "Errore associazione: $e", + ), + ); + } + } } diff --git a/lib/features/operations/blocs/operation_files_events.dart b/lib/features/operations/blocs/operation_files_events.dart index 4542902..44a3b5e 100644 --- a/lib/features/operations/blocs/operation_files_events.dart +++ b/lib/features/operations/blocs/operation_files_events.dart @@ -17,7 +17,7 @@ class OperationsavedEvent extends OperationFilesEvent { class LoadOperationFilesEvent extends OperationFilesEvent { final String? operationId; - final OperationModel? operation; + final AttachmentModel? operation; const LoadOperationFilesEvent({this.operationId, this.operation}); @override @@ -34,23 +34,25 @@ class AddOperationFilesEvent extends OperationFilesEvent { class UploadOperationFilesEvent extends OperationFilesEvent { final List? pickedFiles; - final List? photos; + final List? photos; const UploadOperationFilesEvent({this.pickedFiles, this.photos}); @override List get props => [pickedFiles, photos]; } -class UploadMultipleOperationFilesEvent extends OperationFilesEvent { - final List files; - const UploadMultipleOperationFilesEvent(this.files); +class LinkFilesToCustomerEvent extends OperationFilesEvent { + final String customerId; + + const LinkFilesToCustomerEvent({required this.customerId}); + @override - List get props => [files]; + List get props => [customerId]; } class DeleteOperationFilesEvent extends OperationFilesEvent {} class ToggleOperationFileSelectionEvent extends OperationFilesEvent { - final OperationFileModel file; + final AttachmentModel file; const ToggleOperationFileSelectionEvent(this.file); } diff --git a/lib/features/operations/blocs/operation_files_state.dart b/lib/features/operations/blocs/operation_files_state.dart index 8ea5eb3..a102dd4 100644 --- a/lib/features/operations/blocs/operation_files_state.dart +++ b/lib/features/operations/blocs/operation_files_state.dart @@ -15,10 +15,10 @@ class OperationFilesState extends Equatable { final String? operationId; final OperationFilesStatus status; final String? error; - final List localFiles; - final List remoteFiles; + final List localFiles; + final List remoteFiles; - final List selectedFiles; + final List selectedFiles; @override List get props => [ @@ -30,15 +30,15 @@ class OperationFilesState extends Equatable { selectedFiles, ]; - List get allFiles => [...remoteFiles, ...localFiles]; + List get allFiles => [...remoteFiles, ...localFiles]; OperationFilesState copyWith({ String? operationId, OperationFilesStatus? status, String? error, - List? localFiles, - List? remoteFiles, - List? selectedFiles, + List? localFiles, + List? remoteFiles, + List? selectedFiles, }) { return OperationFilesState( operationId: operationId ?? this.operationId, diff --git a/lib/features/operations/blocs/operations_cubit.dart b/lib/features/operations/blocs/operations_cubit.dart index f66e1b0..cfdf1a6 100644 --- a/lib/features/operations/blocs/operations_cubit.dart +++ b/lib/features/operations/blocs/operations_cubit.dart @@ -4,19 +4,18 @@ 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/utils/extensions.dart'; +import 'package:flux/features/attachments/models/attachment_model.dart'; import 'package:flux/features/operations/data/operations_repository.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'; import 'package:flux/features/operations/models/operation_model.dart'; import 'package:get_it/get_it.dart'; import 'package:collection/collection.dart'; +import 'package:uuid/uuid.dart'; part 'operations_state.dart'; class OperationsCubit extends Cubit { final OperationsRepository _repository = GetIt.I(); final SessionCubit _sessionCubit = GetIt.I(); + final Uuid _uuid = const Uuid(); // Generatore di UUID per il batch OperationsCubit() : super(const OperationsState(status: OperationsStatus.initial)); @@ -24,17 +23,13 @@ class OperationsCubit extends Cubit { // --- CARICAMENTO E PAGINAZIONE --- Future loadOperations({bool refresh = false}) async { - // Se stiamo già caricando, evitiamo chiamate doppie if (state.status == OperationsStatus.loading) return; - - // Se non è un refresh e abbiamo già raggiunto la fine dei dati, ci fermiamo if (!refresh && state.hasReachedMax) return; emit( state.copyWith( status: OperationsStatus.loading, errorMessage: null, - // Se è un refresh, svuotiamo la lista attuale per mostrare lo shimmer/loading allOperations: refresh ? [] : state.allOperations, hasReachedMax: refresh ? false : state.hasReachedMax, ), @@ -56,7 +51,6 @@ class OperationsCubit extends Cubit { dateRange: state.dateRange, ); - // Se ricevi meno record del limite, significa che non ce ne sono altri sul DB final bool reachedMax = newOperations.length < 50; emit( @@ -72,7 +66,7 @@ class OperationsCubit extends Cubit { emit( state.copyWith( status: OperationsStatus.failure, - errorMessage: "Errore nel caricamento servizi: $e", + errorMessage: "Errore nel caricamento operazioni: $e", ), ); } @@ -80,7 +74,6 @@ class OperationsCubit extends Cubit { // --- GESTIONE FILTRI --- - /// Aggiorna i parametri di ricerca e ricarica da zero void updateFilters({String? query, DateTimeRange? range}) { emit( state.copyWith( @@ -91,15 +84,11 @@ class OperationsCubit extends Cubit { loadOperations(refresh: true); } - /// Pulisce tutti i filtri void clearFilters() { emit(state.copyWith(query: '', dateRange: null)); loadOperations(refresh: true); } - // --- GESTIONE BOZZA (DRAFT) --- - - /// Inizializza un nuovo servizio o ne carica uno esistente per la modifica void initOperationForm({ OperationModel? existingOperation, String? operationId, @@ -123,14 +112,16 @@ class OperationsCubit extends Cubit { ), ); } else { - // Crea un template vuoto con lo store di default (se disponibile) + // NUOVA PRATICA: Creiamo un nuovo Batch UUID emit( state.copyWith( currentOperation: OperationModel( storeId: _sessionCubit.state.currentStore?.id ?? '', - number: '', // Sarà compilato dall'utente + reference: '', createdAt: DateTime.now(), companyId: _sessionCubit.state.company!.id!, + status: OperationStatus.draft, + batchUuid: _uuid.v4(), // <-- GENERIAMO IL BATCH UNIVOCO ), status: OperationsStatus.ready, ), @@ -138,68 +129,25 @@ class OperationsCubit extends Cubit { } } - /// Metodo generico per aggiornare i campi base (AL, MNP, Note, ecc.) - void updateField({ - int? al, - int? mnp, - int? nip, - int? unica, - int? telepass, - String? note, - String? number, - bool? isBozza, - bool? resultOk, - String? customerId, - String? customerDisplayName, - }) { + /// MAGIA PURA: Prepara il form per inserire un altro servizio nella stessa pratica. + /// Mantiene il Cliente, il Batch e lo Store, ma svuota il resto. + void prepareNextOperationInBatch() { if (state.currentOperation == null) return; - final updated = state.currentOperation!.copyWith( - al: al, - mnp: mnp, - nip: nip, - unica: unica, - telepass: telepass, - note: note, - number: number, - isBozza: isBozza, - resultOk: resultOk, - customerId: customerId, - customerDisplayName: customerDisplayName, - ); + final current = state.currentOperation!; - emit(state.copyWith(currentOperation: updated)); - } - - // --- GESTIONE MODULI COMPLESSI --- - - void updateEnergyOperations(List energyList) { emit( state.copyWith( - currentOperation: state.currentOperation?.copyWith( - energyOperations: energyList, - ), - ), - ); - } - - void updateFinOperations(List finList) { - emit( - state.copyWith( - currentOperation: state.currentOperation?.copyWith( - finOperations: finList, - ), - ), - ); - } - - void updateEntertainmentOperations( - List entList, - ) { - emit( - state.copyWith( - currentOperation: state.currentOperation?.copyWith( - entertainmentOperations: entList, + status: OperationsStatus.ready, + currentOperation: OperationModel( + companyId: current.companyId, + storeId: current.storeId, + storeDisplayName: current.storeDisplayName, + batchUuid: current.batchUuid, // <-- MANTIENE IL COLLEGAMENTO + customerId: current.customerId, // <-- MANTIENE IL CLIENTE + customerDisplayName: current.customerDisplayName, + status: OperationStatus.draft, + createdAt: DateTime.now(), ), ), ); @@ -208,35 +156,33 @@ class OperationsCubit extends Cubit { // --- PERSISTENZA --- Future saveCurrentOperation({ - required bool isBozza, + required OperationStatus targetStatus, bool shouldPop = true, - List? files, }) async { if (state.currentOperation == null) return; emit(state.copyWith(status: OperationsStatus.saving, errorMessage: null)); try { - // 1. Aggiorniamo il flag bozza in base a quale pulsante ha premuto l'utente final operationToSave = state.currentOperation!.copyWith( - isBozza: isBozza, - files: files, + status: targetStatus, ); - // 2. Salvataggio corazzato final updatedOperation = await _repository.saveFullOperation( operationToSave, ); - // 3. Reset e ricaricamento emit( state.copyWith( + // Se non facciamo Pop (es. l'utente vuole aggiungere un altro servizio), non killiamo l'operazione corrente status: shouldPop ? OperationsStatus.saved : OperationsStatus.savedNoPop, currentOperation: shouldPop ? null : updatedOperation, ), ); - await loadOperations(refresh: true); + + // Ricarica in background per la dashboard + loadOperations(refresh: true); } catch (e) { emit( state.copyWith( @@ -247,115 +193,29 @@ class OperationsCubit extends Cubit { } } - // --- GESTIONE ALLEGATI LOCALI --- + // --- RECUPERO OPERAZIONI DELLO STESSO BATCH (Per UI di riepilogo) --- - void addAttachments(List files) { - final newAttachments = files.map((file) { - return OperationFileModel( - id: null, // Meglio null se non è su DB - operationId: state.currentOperation?.id ?? '', - name: file.name.fileNameWithoutExtension(), - extension: file.name.fileExtension(), - storagePath: '', - fileSize: file.size, - localBytes: file.bytes, - createdAt: DateTime.now(), - ); - }).toList(); + /// Puoi usare questa funzione se nella UI vuoi mostrare "Hai inserito 3 servizi in questa pratica" + List getOperationsInCurrentBatch() { + if (state.currentOperation == null) return []; + final currentBatch = state.currentOperation!.batchUuid; - // Creiamo una nuova lista pulita - final List updatedList = [ - ...(state.currentOperation?.files ?? []), - ...newAttachments, - ]; - - // Emettiamo lo stato assicurandoci che il OperationModel venga clonato - if (state.currentOperation != null) { - emit( - state.copyWith( - currentOperation: state.currentOperation!.copyWith( - files: updatedList, - ), - ), - ); - } + // Filtriamo dalla lista caricata (o potresti fare una query diretta a Supabase se preferisci) + return state.allOperations + .where( + (op) => + op.batchUuid == currentBatch && + op.id != state.currentOperation!.id, + ) + .toList(); } - void removeAttachment(int index) { + void updateField({String? customerId, String? customerDisplayName}) { if (state.currentOperation == null) return; - - final updatedList = List.from( - state.currentOperation!.files, + final updated = state.currentOperation!.copyWith( + customerId: customerId, + customerDisplayName: customerDisplayName, ); - updatedList.removeAt(index); - - emit( - state.copyWith( - currentOperation: state.currentOperation?.copyWith(files: updatedList), - ), - ); - } - - void saveAndCopyFileToCustomer(List selectedFiles) async { - final currentOperation = state.currentOperation; - - // 1. Check di sicurezza: se non c'è il cliente, non sappiamo dove copiare - if (currentOperation == null || currentOperation.customerId == null) { - emit( - state.copyWith( - status: OperationsStatus.failure, - errorMessage: - "Impossibile copiare: nessun cliente associato alla pratica.", - ), - ); - return; - } - - emit(state.copyWith(status: OperationsStatus.loading)); - - try { - // 2. SALVATAGGIO CORAZZATO - // Chiamiamo il repo e otteniamo la pratica con TUTTI i file ora dotati di ID e storagePath - final updatedOperation = await _repository.saveFullOperation( - currentOperation, - ); - - // 3. COPIA RELAZIONALE - // Per ogni file che l'utente ha selezionato nella UI, cerchiamo la sua versione - // "ufficiale" (quella con lo storagePath) nel modello appena tornato dal DB. - for (var selectedFile in selectedFiles) { - // Cerchiamo il match nel modello aggiornato - final persistedFile = updatedOperation.files.firstWhere( - (f) => - f.name == selectedFile.name && - f.extension == selectedFile.extension, - orElse: () => throw Exception( - "File ${selectedFile.name} non trovato dopo il salvataggio.", - ), - ); - - // Creiamo il link nel database del cliente - await _repository.copyFileToCustomer( - file: persistedFile, - customerId: currentOperation.customerId!, - ); - } - - // 4. AGGIORNAMENTO STATO - // Aggiorniamo il Cubit con il servizio salvato così la UI mostra i file come "Remoti" - emit( - state.copyWith( - status: OperationsStatus.success, - currentOperation: updatedOperation, - ), - ); - } catch (e) { - emit( - state.copyWith( - status: OperationsStatus.failure, - errorMessage: "Errore durante il salvataggio e copia: $e", - ), - ); - } + emit(state.copyWith(currentOperation: updated)); } } diff --git a/lib/features/operations/data/operations_repository.dart b/lib/features/operations/data/operations_repository.dart index d4298ad..e846ca5 100644 --- a/lib/features/operations/data/operations_repository.dart +++ b/lib/features/operations/data/operations_repository.dart @@ -3,9 +3,6 @@ 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'; import 'package:get_it/get_it.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import '../models/operation_model.dart'; @@ -13,7 +10,6 @@ import '../models/operation_model.dart'; class OperationsRepository { final _supabase = Supabase.instance.client; final companyId = GetIt.I.get().state.company!.id; - final CustomerRepository _customerRepository = GetIt.I(); // --- RECUPERO SINGOLO SERVIZIO CON JOIN COMPLETO --- Future fetchOperationById(String id) async { @@ -23,7 +19,11 @@ class OperationsRepository { .select(''' *, customer(name), - staff_member(name) + store(name), + staff_member(name), + provider(name), + model(name_with_brand), + attachments(*) ''') .eq('id', id) .single(); @@ -48,6 +48,9 @@ class OperationsRepository { .select(''' *, customer(name), + store(name), + provider(name), + model(name_with_brand), staff_member(name), attachments(*) ''') @@ -107,49 +110,6 @@ class OperationsRepository { final String newId = operationData['id']; - // 4. UPLOAD DEI FILE LOCALI (Nuovi) - // Filtriamo solo i file che non hanno ancora un ID (quindi sono locali) - final localFilesToUpload = operation.attachments - .where((f) => f.id == null) - .toList(); - - if (localFilesToUpload.isNotEmpty) { - final List uploadTasks = []; - - for (var file in localFilesToUpload) { - final storagePath = - '$companyId/operations/$newId/${DateTime.now().millisecondsSinceEpoch}_${file.name}.${file.extension}'; - final String mimeType = file.extension.toLowerCase() == 'pdf' - ? 'application/pdf' - : 'image/${file.extension}'; - - final fileToSave = file.copyWith( - operationId: newId, - storagePath: storagePath, - ); - - // Creiamo una funzione asincrona per caricare file e scrivere nel DB - Future uploadAndLink() async { - // A. Upload nel Bucket Storage (usiamo i bytes così funziona anche su Web!) - await _supabase.storage - .from('documents') - .uploadBinary( - storagePath, - fileToSave.localBytes!, - fileOptions: FileOptions(contentType: mimeType, upsert: true), - ); - - // B. Inserimento riga nel DB relazionale - await _supabase.from('attachment').insert(fileToSave.toMap()); - } - - uploadTasks.add(uploadAndLink()); - } - - // Eseguiamo tutti gli upload in parallelo per la massima velocità - await Future.wait(uploadTasks); - } - // 5. GRAN FINALE: RECUPERO DEL MODELLO COMPLETO E AGGIORNATO // Interroghiamo Supabase per farci restituire la pratica con TUTTI gli ID generati // (inclusi quelli della tabella operation_file appena inseriti) @@ -158,6 +118,9 @@ class OperationsRepository { .select(''' *, staff_member(name), + store(name), + provider(name), + model(name_with_brand), customer(name), attachments(*) ''') @@ -278,7 +241,7 @@ class OperationsRepository { } Future copyFileToCustomer({ - required OperationFileModel file, + required AttachmentModel file, required String customerId, }) async { await _supabase @@ -290,16 +253,28 @@ class OperationsRepository { 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(); - final List storagePaths = files.map((f) => f.storagePath).toList(); - + final List idsToDelete = []; + final List idsToEdit = []; + final List storagePathsToDelete = []; + for (var file in files) { + if (file.customerId == null) { + idsToDelete.add(file.id!); + storagePathsToDelete.add(file.storagePath); + } else { + idsToEdit.add(file.id!); + } + } try { - await _supabase - .from('attachment') - .update({'operation_id': null}) - .inFilter('id', idsToDelete); - - await _supabase.storage.from('documents').remove(storagePaths); + if (idsToDelete.isNotEmpty) { + await _supabase.from('attachment').delete().inFilter('id', idsToDelete); + await _supabase.storage.from('documents').remove(storagePathsToDelete); + } + if (idsToEdit.isNotEmpty) { + await _supabase + .from('attachment') + .update({'operation_id': null}) + .inFilter('id', idsToEdit); + } } on PostgrestException catch (e) { throw 'Errore database: ${e.message}'; } catch (e) { diff --git a/lib/features/operations/models/operation_file_model.dart b/lib/features/operations/models/operation_file_model.dart deleted file mode 100644 index 376c8c1..0000000 --- a/lib/features/operations/models/operation_file_model.dart +++ /dev/null @@ -1,100 +0,0 @@ -import 'dart:typed_data'; - -import 'package:equatable/equatable.dart'; - -class OperationFileModel extends Equatable { - final String? id; - final DateTime? createdAt; - final String name; - final String extension; - final String storagePath; - final String operationId; - final int fileSize; - final Uint8List? localBytes; - - const OperationFileModel({ - this.id, - this.createdAt, - required this.name, - required this.extension, - required this.storagePath, - required this.operationId, - required this.fileSize, - this.localBytes, - }); - - bool get isLocal => localBytes != null; - - // 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'; - - OperationFileModel copyWith({ - String? id, - DateTime? createdAt, - String? name, - String? extension, - String? storagePath, - String? operationId, - int? fileSize, - Uint8List? localBytes, - }) { - return OperationFileModel( - id: id ?? this.id, - createdAt: createdAt ?? this.createdAt, - name: name ?? this.name, - extension: extension ?? this.extension, - storagePath: storagePath ?? this.storagePath, - operationId: operationId ?? this.operationId, - fileSize: fileSize ?? this.fileSize, - localBytes: localBytes ?? this.localBytes, - ); - } - - factory OperationFileModel.fromMap(Map map) { - return OperationFileModel( - id: map['id'] as String, - createdAt: map['created_at'] != null - ? DateTime.parse(map['created_at']) - : null, - name: map['name'] ?? '', - extension: map['extension'] ?? '', - storagePath: map['storage_path'] ?? '', - operationId: map['operation_id']?.toString() ?? '', - fileSize: map['file_size'] is int - ? map['file_size'] - : int.tryParse(map['file_size']?.toString() ?? '0') ?? 0, - ); - } - - Map toMap() { - return { - if (id != null) 'id': id, - 'name': name, - 'extension': extension, - 'storage_path': storagePath, - 'operation_id': operationId, - 'file_size': fileSize, - }; - } - - @override - List get props => [ - id, - createdAt, - name, - extension, - storagePath, - operationId, - fileSize, - localBytes, - ]; -} diff --git a/lib/features/operations/models/operation_model.dart b/lib/features/operations/models/operation_model.dart index 6f304a1..7ffe874 100644 --- a/lib/features/operations/models/operation_model.dart +++ b/lib/features/operations/models/operation_model.dart @@ -1,4 +1,5 @@ import 'package:equatable/equatable.dart'; +import 'package:flux/core/utils/extensions.dart'; import 'package:flux/features/attachments/models/attachment_model.dart'; enum OperationStatus { @@ -27,7 +28,9 @@ class OperationModel extends Equatable { final DateTime? createdAt; final String type; final String? providerId; + final String? providerDisplayName; final String? modelId; + final String? modelDisplayName; final String? description; final DateTime? expirationDate; final String note; @@ -35,13 +38,14 @@ class OperationModel extends Equatable { final String batchUuid; final String companyId; final String storeId; + final String? storeDisplayName; final int quantity; final String? staffId; - final String staffDisplayName; + final String? staffDisplayName; final String? lastCampaignId; final OperationStatus status; final String? customerId; - final String customerDisplayName; + final String? customerDisplayName; final String reference; // ALLEGATI (Aggiunto) @@ -52,7 +56,9 @@ class OperationModel extends Equatable { this.createdAt, this.type = '', this.providerId, + this.providerDisplayName, this.modelId, + this.modelDisplayName, this.description, this.expirationDate, this.note = '', @@ -60,13 +66,14 @@ class OperationModel extends Equatable { this.batchUuid = '', required this.companyId, this.storeId = '', + this.storeDisplayName, this.quantity = 1, this.staffId, - this.staffDisplayName = '', + this.staffDisplayName, this.lastCampaignId, this.status = OperationStatus.draft, this.customerId, - this.customerDisplayName = '', + this.customerDisplayName, this.reference = '', this.attachments = const [], }); @@ -76,7 +83,9 @@ class OperationModel extends Equatable { DateTime? createdAt, String? type, String? providerId, + String? providerDisplayName, String? modelId, + String? modelDisplayName, String? description, DateTime? expirationDate, String? note, @@ -84,6 +93,7 @@ class OperationModel extends Equatable { String? batchUuid, String? companyId, String? storeId, + String? storeDisplayName, int? quantity, String? staffId, String? staffDisplayName, @@ -98,7 +108,9 @@ class OperationModel extends Equatable { createdAt: createdAt ?? this.createdAt, type: type ?? this.type, providerId: providerId ?? this.providerId, + providerDisplayName: providerDisplayName ?? this.providerDisplayName, modelId: modelId ?? this.modelId, + modelDisplayName: modelDisplayName ?? this.modelDisplayName, description: description ?? this.description, expirationDate: expirationDate ?? this.expirationDate, note: note ?? this.note, @@ -106,6 +118,7 @@ class OperationModel extends Equatable { batchUuid: batchUuid ?? this.batchUuid, companyId: companyId ?? this.companyId, storeId: storeId ?? this.storeId, + storeDisplayName: storeDisplayName ?? this.storeDisplayName, quantity: quantity ?? this.quantity, staffId: staffId ?? this.staffId, staffDisplayName: staffDisplayName ?? this.staffDisplayName, @@ -123,7 +136,9 @@ class OperationModel extends Equatable { createdAt, type, providerId, + providerDisplayName, modelId, + modelDisplayName, description, expirationDate, note, @@ -131,6 +146,7 @@ class OperationModel extends Equatable { batchUuid, companyId, storeId, + storeDisplayName, quantity, staffId, staffDisplayName, @@ -154,7 +170,9 @@ class OperationModel extends Equatable { : null, type: map['type'] as String? ?? '', providerId: map['provider_id'] as String? ?? '', + providerDisplayName: "${map['provider']['name']}".myFormat(), modelId: map['model_id'] as String? ?? '', + modelDisplayName: "${map['model']['name_with_brand'] ?? ''}".myFormat(), description: map['description'] as String? ?? '', expirationDate: map['expiration_date'] != null ? DateTime.parse(map['expiration_date']) @@ -164,13 +182,22 @@ class OperationModel extends Equatable { batchUuid: map['batch_uuid'] as String, companyId: map['company_id'] as String, storeId: map['store_id'] as String? ?? '', + storeDisplayName: "${map['store']['name']}".myFormat(), quantity: map['quantity'] is int ? map['quantity'] : int.tryParse(map['quantity']?.toString() ?? '0') ?? 0, staffId: map['staff_id'] as String? ?? '', + staffDisplayName: "${map['staff_member']['name'] ?? ''}".myFormat(), lastCampaignId: map['last_campaign_id'] as String? ?? '', status: OperationStatus.fromString(map['status']), customerId: map['customer_id'] as String? ?? '', + customerDisplayName: "${map['customer']['name'] ?? ''}".myFormat(), + attachments: + (map['attachment'] as List?) + ?.map((x) => AttachmentModel.fromMap(x)) + .toList() ?? + const [], + reference: map['reference'] as String? ?? '', ); } diff --git a/lib/features/operations/ui/operation_form_screen/attachment_section.dart b/lib/features/operations/ui/operation_form_screen/attachment_section.dart index 5980313..7933045 100644 --- a/lib/features/operations/ui/operation_form_screen/attachment_section.dart +++ b/lib/features/operations/ui/operation_form_screen/attachment_section.dart @@ -2,12 +2,14 @@ import 'package:file_picker/file_picker.dart'; 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/utils/extensions.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/operations/blocs/operation_files_bloc.dart'; import 'package:flux/features/operations/blocs/operations_cubit.dart'; -import 'package:flux/features/operations/models/operation_file_model.dart'; +import 'package:flux/features/operations/models/operation_model.dart'; class AttachmentsSection extends StatelessWidget { const AttachmentsSection({super.key}); @@ -227,10 +229,30 @@ class AttachmentsSection extends StatelessWidget { ElevatedButton.icon( icon: const Icon(Icons.copy), label: const Text("Copia in Cliente"), - onPressed: () => saveAndCopyFilesToCustomer( - context, - state.selectedFiles, - ), + onPressed: () { + final cubit = context.read(); + if (cubit.state.currentOperation?.customerId == + null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + context + .l10n + .operationFormAttachmentSectionNoCustomer, + ), + ), + ); + } else { + context.read().add( + LinkFilesToCustomerEvent( + customerId: cubit + .state + .currentOperation! + .customerId!, + ), + ); + } + }, ), ], ), @@ -278,9 +300,8 @@ class AttachmentsSection extends StatelessWidget { // Salviamo forzatamente in bozza await cubit.saveCurrentOperation( - isBozza: true, + targetStatus: OperationStatus.draft, shouldPop: false, - files: operationFilesBloc.state.localFiles, ); // Recuperiamo il servizio aggiornato con l'ID! @@ -321,39 +342,8 @@ class AttachmentsSection extends StatelessWidget { } } - // --- LOGICA DI COPIA AL CLIENTE --- - void saveAndCopyFilesToCustomer( - BuildContext context, - List files, - ) { - showDialog( - context: context, - builder: (ctx) => AlertDialog( - title: const Text("Copia nei documenti Cliente"), - content: const Text( - "Vuoi copiare i file selezionati nell'anagrafica del cliente? \n\n" - "Attenzione: per procedere, la pratica attuale verrà prima salvata in stato BOZZA.", - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(ctx), - child: const Text("Annulla"), - ), - ElevatedButton( - onPressed: () { - Navigator.pop(ctx); - // 1. Diciamo al Cubit di salvare in Bozza e fare la copia - context.read().saveAndCopyFileToCustomer(files); - }, - child: const Text("Salva e Copia"), - ), - ], - ), - ); - } - // --- LOGICA DI VISUALIZZAZIONE OVERLAY --- - void _handleDoubleClick(BuildContext context, OperationFileModel file) { + void _handleDoubleClick(BuildContext context, AttachmentModel file) { showDialog( context: context, barrierDismissible: true, diff --git a/lib/features/operations/ui/operation_form_screen/general_info_section.dart b/lib/features/operations/ui/operation_form_screen/general_info_section.dart index ceaf3db..183224c 100644 --- a/lib/features/operations/ui/operation_form_screen/general_info_section.dart +++ b/lib/features/operations/ui/operation_form_screen/general_info_section.dart @@ -1,6 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/features/operations/blocs/operations_cubit.dart'; import 'package:flux/features/operations/models/operation_model.dart'; class GeneralInfoSection extends StatelessWidget { @@ -34,7 +32,7 @@ class GeneralInfoSection extends StatelessWidget { // Numero di Riferimento / Telefono TextFormField( - initialValue: operation.number, + initialValue: operation.reference, keyboardType: TextInputType .phone, // Fa aprire il tastierino numerico su mobile decoration: const InputDecoration( @@ -43,49 +41,6 @@ class GeneralInfoSection extends StatelessWidget { border: OutlineInputBorder(), prefixIcon: Icon(Icons.phone), ), - onChanged: (val) { - context.read().updateField(number: val); - }, - ), - const SizedBox(height: 16), - - // I due Switch affiancati (Bozza e A buon fine) - Row( - children: [ - Expanded( - child: SwitchListTile( - title: const Text("Bozza"), - subtitle: const Text( - "Pratica in lavorazione", - style: TextStyle(fontSize: 12), - ), - value: operation.isBozza, - activeThumbColor: Colors.orange, - contentPadding: EdgeInsets.zero, - onChanged: (val) { - context.read().updateField(isBozza: val); - }, - ), - ), - const SizedBox(width: 16), - Expanded( - child: SwitchListTile( - title: const Text("A buon fine"), - subtitle: const Text( - "Esito positivo", - style: TextStyle(fontSize: 12), - ), - value: operation.resultOk, - activeThumbColor: Colors.green, - contentPadding: EdgeInsets.zero, - onChanged: (val) { - context.read().updateField( - resultOk: val, - ); - }, - ), - ), - ], ), const SizedBox(height: 16), @@ -101,9 +56,6 @@ class GeneralInfoSection extends StatelessWidget { border: OutlineInputBorder(), alignLabelWithHint: true, ), - onChanged: (val) { - context.read().updateField(note: val); - }, ), ], ), diff --git a/lib/features/operations/ui/operation_form_screen/operation_form_screen.dart b/lib/features/operations/ui/operation_form_screen/operation_form_screen.dart index 4608a9a..b7379e3 100644 --- a/lib/features/operations/ui/operation_form_screen/operation_form_screen.dart +++ b/lib/features/operations/ui/operation_form_screen/operation_form_screen.dart @@ -5,7 +5,6 @@ import 'package:flux/features/operations/models/operation_model.dart'; import 'package:flux/features/operations/ui/operation_form_screen/attachment_section.dart'; import 'package:flux/features/operations/ui/operation_form_screen/customer_section.dart'; import 'package:flux/features/operations/ui/operation_form_screen/general_info_section.dart'; -import 'package:flux/features/operations/ui/operation_form_screen/operations_grid.dart'; class OperationFormScreen extends StatefulWidget { final String? operationId; @@ -34,9 +33,16 @@ class _OperationFormScreenState extends State { }); } - void _performSave(BuildContext context, {required bool isBozza}) { + void _performSave( + BuildContext context, { + required OperationStatus targetStatus, + required bool shouldPop, + }) { FocusScope.of(context).unfocus(); - context.read().saveCurrentOperation(isBozza: isBozza); + context.read().saveCurrentOperation( + targetStatus: targetStatus, + shouldPop: shouldPop, + ); } @override @@ -93,7 +99,11 @@ class _OperationFormScreenState extends State { IconButton( icon: const Icon(Icons.edit_note), tooltip: "Salva come Bozza", - onPressed: () => _performSave(context, isBozza: true), + onPressed: () => _performSave( + context, + targetStatus: OperationStatus.draft, + shouldPop: false, + ), ), IconButton( icon: const Icon( @@ -101,7 +111,11 @@ class _OperationFormScreenState extends State { color: Colors.green, ), tooltip: "Conferma Pratica", - onPressed: () => _performSave(context, isBozza: false), + onPressed: () => _performSave( + context, + targetStatus: OperationStatus.ok, + shouldPop: true, + ), ), const SizedBox(width: 8), ], @@ -120,9 +134,6 @@ class _OperationFormScreenState extends State { GeneralInfoSection(operation: operation), const SizedBox(height: 24), - OperationsGrid(operation: operation), - const SizedBox(height: 32), - AttachmentsSection(), const SizedBox(height: 32), _buildBottomActionButtons(context, isSaving: isSaving), @@ -152,7 +163,11 @@ class _OperationFormScreenState extends State { label: const Text("Salva in Bozza"), onPressed: isSaving ? null - : () => _performSave(context, isBozza: true), + : () => _performSave( + context, + targetStatus: OperationStatus.draft, + shouldPop: false, + ), ), ), @@ -173,7 +188,11 @@ class _OperationFormScreenState extends State { ), onPressed: isSaving ? null - : () => _performSave(context, isBozza: false), + : () => _performSave( + context, + targetStatus: OperationStatus.ok, + shouldPop: true, + ), ), ), ], diff --git a/lib/features/operations/ui/operation_form_screen/operation_mobile_upload_screen.dart b/lib/features/operations/ui/operation_form_screen/operation_mobile_upload_screen.dart index 8adb21c..ad3fde1 100644 --- a/lib/features/operations/ui/operation_form_screen/operation_mobile_upload_screen.dart +++ b/lib/features/operations/ui/operation_form_screen/operation_mobile_upload_screen.dart @@ -296,7 +296,7 @@ class _OperationMobileUploadScreenState // Diciamo al BLoC di caricare tutti i file. // Usiamo il tuo evento esistente per ogni file (il BLoC li metterà in coda) final bloc = context.read(); - bloc.add(UploadMultipleOperationFilesEvent(_stagedFiles)); + bloc.add(UploadOperationFilesEvent(pickedFiles: _stagedFiles)); // N.B: Il Navigator.pop() viene chiamato dal BlocListener in alto quando lo stato diventa "success"! } diff --git a/lib/features/operations/ui/operation_form_screen/operations_grid.dart b/lib/features/operations/ui/operation_form_screen/operations_grid.dart deleted file mode 100644 index e5e252e..0000000 --- a/lib/features/operations/ui/operation_form_screen/operations_grid.dart +++ /dev/null @@ -1,201 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/features/master_data/products/blocs/product_cubit.dart'; -import 'package:flux/features/operations/blocs/operations_cubit.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_model.dart'; -import 'package:flux/features/operations/ui/operation_form_screen/action_card.dart'; -import 'package:flux/features/operations/ui/operation_form_screen/energy_operation_dialog.dart'; -import 'package:flux/features/operations/ui/operation_form_screen/entertainment_operation_card.dart'; -import 'package:flux/features/operations/ui/operation_form_screen/finance_operation_dialog.dart'; -import 'package:flux/features/operations/ui/operation_form_screen/int_dialogs.dart'; // Assicurati di importare il modello - -class OperationsGrid extends StatelessWidget { - final OperationModel operation; - - const OperationsGrid({super.key, required this.operation}); - - @override - Widget build(BuildContext context) { - return Card( - elevation: 2, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - - children: [ - Row( - children: [ - Icon( - Icons.layers_outlined, - color: Theme.of(context).colorScheme.primary, - ), - const SizedBox(width: 8), - Text( - "Servizi e Accessori", - style: Theme.of(context).textTheme.titleMedium, - ), - ], - ), - const SizedBox(height: 20), - SizedBox( - width: double.infinity, - child: Wrap( - spacing: 16, - runSpacing: 16, - alignment: WrapAlignment.center, - children: [ - // --- CONTATORI SEMPLICI --- - ActionCard( - label: "AL", - count: operation.al, - icon: Icons.sim_card, - color: Colors.blue, - onTap: () => updateCountDialog( - context, - "AL", - operation.al, - (val) => - context.read().updateField(al: val), - ), - ), - ActionCard( - label: "MNP", - count: operation.mnp, - icon: Icons.phone_android, - color: Colors.indigo, - onTap: () => updateCountDialog( - context, - "MNP", - operation.mnp, - (val) => - context.read().updateField(mnp: val), - ), - ), - ActionCard( - label: "NIP", - count: operation.nip, - icon: Icons.compare_arrows, - color: Colors.cyan, - onTap: () => updateCountDialog( - context, - "NIP", - operation.nip, - (val) => - context.read().updateField(nip: val), - ), - ), - ActionCard( - label: "Unica", - count: operation.unica, - icon: Icons.all_inclusive, - color: Colors.purple, - onTap: () => updateCountDialog( - context, - "Unica", - operation.unica, - (val) => context.read().updateField( - unica: val, - ), - ), - ), - ActionCard( - label: "Telepass", - count: operation.telepass, - icon: Icons.directions_car, - color: Colors.amber.shade700, - onTap: () => updateCountDialog( - context, - "Telepass", - operation.telepass, - (val) => context.read().updateField( - telepass: val, - ), - ), - ), - - // --- MODULI COMPLESSI (Le liste) --- - ActionCard( - label: "Energia", - count: operation.energyOperations.length, - icon: Icons.bolt, - color: Colors.green, - onTap: () async { - // Apriamo la modale e aspettiamo il risultato - final result = - await showDialog>( - context: context, - builder: (context) => EnergyOperationDialog( - currentStoreId: operation.storeId, - initialOperations: operation - .energyOperations, // Passiamo la lista attuale - ), - ); - - // Se l'utente ha premuto "Conferma" e non "Annulla" o tap fuori - if (result != null && context.mounted) { - context.read().updateEnergyOperations( - result, - ); - } - }, - ), - ActionCard( - label: "Finanziam.", - count: operation.finOperations.length, - icon: Icons.euro_symbol, - color: Colors.teal, - onTap: () async { - final result = await showDialog>( - context: context, - builder: (context) => FinanceOperationDialog( - productCubit: context.read(), - currentStoreId: operation.storeId, - initialOperations: operation - .finOperations, // Passiamo la lista attuale - ), - ); - - if (result != null && context.mounted) { - context.read().updateFinOperations( - result, - ); - } - }, - ), - ActionCard( - label: "Intratten.", - count: operation.entertainmentOperations.length, - icon: Icons.movie_filter_outlined, - color: Colors.purple, - onTap: () async { - final result = - await showDialog>( - context: context, - builder: (context) => EntertainmentOperationDialog( - initialOperations: - operation.entertainmentOperations, - currentStoreId: operation.storeId, - ), - ); - - if (result != null && context.mounted) { - context - .read() - .updateEntertainmentOperations(result); - } - }, - ), - ], - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/features/operations/ui/operations_screen.dart b/lib/features/operations/ui/operations_screen.dart index a56e73e..d2d5eed 100644 --- a/lib/features/operations/ui/operations_screen.dart +++ b/lib/features/operations/ui/operations_screen.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/features/operations/blocs/operations_cubit.dart'; import 'package:flux/features/operations/models/operation_model.dart'; -import 'package:flux/features/operations/utils/operation_actions.dart'; import 'package:go_router/go_router.dart'; // Importa i tuoi modelli e cubit @@ -139,15 +138,6 @@ class _OperationsScreenState extends State { ), ), ), - if (operation.isBozza) - const Chip( - label: Text( - "BOZZA", - style: TextStyle(fontSize: 10, color: Colors.white), - ), - backgroundColor: Colors.orange, - visualDensity: VisualDensity.compact, - ), ], ), subtitle: Column( @@ -155,21 +145,14 @@ class _OperationsScreenState extends State { children: [ const SizedBox(height: 4), Text( - "Pratica: ${operation.number} • ${operation.createdAt?.day}/${operation.createdAt?.month}/${operation.createdAt?.year}", + "Pratica: ${operation.reference} • ${operation.createdAt?.day}/${operation.createdAt?.month}/${operation.createdAt?.year}", ), const SizedBox(height: 8), - // I nostri mini-chip per i servizi attivati - Wrap( - spacing: 6, + Row( children: [ - if (operation.al > 0 || operation.mnp > 0) - _miniBadge("📞 Tel", Colors.blue), - if (operation.energyOperations.isNotEmpty) - _miniBadge("⚡ Energy", Colors.green), - if (operation.finOperations.isNotEmpty) - _miniBadge("💰 Fin", Colors.purple), - if (operation.entertainmentOperations.isNotEmpty) - _miniBadge("📺 Ent", Colors.red), + Text(operation.type), + const SizedBox(width: 8), + _buildOperationStatus(operation.status), ], ), ], @@ -187,22 +170,31 @@ class _OperationsScreenState extends State { ); } - Widget _miniBadge(String text, Color color) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: color.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(4), - border: Border.all(color: color.withValues(alpha: 0.5)), - ), - child: Text( - text, - style: TextStyle( - color: color, - fontSize: 10, - fontWeight: FontWeight.bold, - ), - ), + Widget _buildOperationStatus(OperationStatus status) { + Color color; + switch (status) { + case OperationStatus.canceled || OperationStatus.ko: + color = Colors.grey.shade800; + break; + case OperationStatus.waitingforaction || OperationStatus.draft: + color = Colors.orange; + break; + case OperationStatus.ok: + color = Colors.green; + break; + case OperationStatus.waitingfordeployment || + OperationStatus.waitingforsupport: + color = Colors.blue; + break; + } + return Chip( + label: Text("BOZZA", style: TextStyle(fontSize: 10, color: Colors.white)), + backgroundColor: color, + visualDensity: VisualDensity.compact, ); } + + void startNewOperation(BuildContext context) { + context.pushNamed('operation-form'); + } } diff --git a/lib/features/operations/utils/operation_actions.dart b/lib/features/operations/utils/operation_actions.dart deleted file mode 100644 index 35396ae..0000000 --- a/lib/features/operations/utils/operation_actions.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/core/blocs/session/session_cubit.dart'; -import 'package:flux/features/master_data/store/bloc/store_cubit.dart'; -import 'package:flux/features/operations/blocs/operations_cubit.dart'; -import 'package:flux/features/operations/models/operation_model.dart'; -import 'package:go_router/go_router.dart'; - -/// Avvia la creazione di un nuovo servizio partendo dalla selezione dell'operatore. -void startNewOperation(BuildContext context) { - final session = context.read().state; - final currentStoreId = session.currentStore?.id; - - if (currentStoreId == null) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text("Seleziona uno store prima di iniziare")), - ); - return; - } - - showModalBottomSheet( - context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(20)), - ), - builder: (modalContext) { - // Usiamo lo StoreCubit invece dello StaffCubit! - return BlocBuilder( - builder: (context, storeState) { - // Recuperiamo lo staff assegnato a questo specifico store usando la mappa che avevi già creato - final storeStaff = storeState.staffByStore[currentStoreId] ?? []; - - return Container( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Text( - "Chi sta eseguendo l'operazione?", - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 20), - - if (storeStaff.isEmpty) - const Text( - "Nessun membro dello staff configurato per questo store.\nVai in Anagrafica > Negozi per assegnare il personale.", - textAlign: TextAlign.center, - ), - - ...storeStaff.map( - (member) => ListTile( - leading: const CircleAvatar(child: Icon(Icons.person)), - title: Text(member.name), - onTap: () { - // 1. Inizializza il form nel Cubit - context.read().initOperationForm( - existingOperation: OperationModel( - storeId: currentStoreId, - employeeId: member.id, - number: '', - createdAt: DateTime.now(), - companyId: session.company!.id!, - ), - ); - - // 2. Chiudi la modal - Navigator.pop(modalContext); - - // 3. Naviga verso il form - context.pushNamed('operation-form'); - }, - ), - ), - const SizedBox(height: 16), - ], - ), - ); - }, - ); - }, - ); -} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb deleted file mode 100644 index 2a57a54..0000000 --- a/lib/l10n/app_en.arb +++ /dev/null @@ -1,13 +0,0 @@ -{ - "@@locale": "en", - "welcomeBack": "Welcome back, {name}! 👋", - "latestOperations": "Latest Operations", - "masterData": "Master Data", - "settings": "Settings", - "newOperation": "Operation", - "expiring_contracts": "Expiring Contracts", - "sticky_notes": "Sticky Notes", - "my_tasks": "My Tasks", - "latest_operation_tickets": "Latest operation tickets" - -} \ No newline at end of file diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index a211adf..3e195ea 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -86,5 +86,6 @@ "createCompanyScreenWillBeUsedForReceipts": "Verrà utilizzato per le tue stampe e ricevute", "createCompanyScreenSaveCompany": "SALVA AZIENDA", "createCompanyScreenSetupYourCompany": "Configura la tua Azienda", - "createCompanyScreenFluxNeedsYourFiscalData": "FLUX ha bisogno dei tuoi dati fiscali per gestire correttamente le fatturazioni e le attivazioni dei tuoi negozi." + "createCompanyScreenFluxNeedsYourFiscalData": "FLUX ha bisogno dei tuoi dati fiscali per gestire correttamente le fatturazioni e le attivazioni dei tuoi negozi.", + "operationFormAttachmentSectionNoCustomer": "Devi prima selezionare un cliente" } \ No newline at end of file diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 1a5f96f..707d88f 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -441,6 +441,12 @@ abstract class AppLocalizations { /// In it, this message translates to: /// **'FLUX ha bisogno dei tuoi dati fiscali per gestire correttamente le fatturazioni e le attivazioni dei tuoi negozi.'** String get createCompanyScreenFluxNeedsYourFiscalData; + + /// No description provided for @operationFormAttachmentSectionNoCustomer. + /// + /// In it, this message translates to: + /// **'Devi prima selezionare un cliente'** + String get operationFormAttachmentSectionNoCustomer; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index c226972..563bdce 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -199,4 +199,8 @@ class AppLocalizationsIt extends AppLocalizations { @override String get createCompanyScreenFluxNeedsYourFiscalData => 'FLUX ha bisogno dei tuoi dati fiscali per gestire correttamente le fatturazioni e le attivazioni dei tuoi negozi.'; + + @override + String get operationFormAttachmentSectionNoCustomer => + 'Devi prima selezionare un cliente'; } diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb deleted file mode 100644 index 9e26dfe..0000000 --- a/lib/l10n/intl_en.arb +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 30588e2..3573306 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1035,7 +1035,7 @@ packages: source: hosted version: "3.1.5" uuid: - dependency: transitive + dependency: "direct main" description: name: uuid sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" diff --git a/pubspec.yaml b/pubspec.yaml index f511d7a..9a63d01 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,6 +28,7 @@ dependencies: qr_flutter: ^4.1.0 shared_preferences: ^2.5.5 supabase_flutter: ^2.12.2 + uuid: ^4.5.3 dev_dependencies: flutter_test: