diff --git a/lib/core/widgets/shared_forms/attachments_section.dart b/lib/core/widgets/shared_forms/attachments_section.dart index b2fa64f..450ef0e 100644 --- a/lib/core/widgets/shared_forms/attachments_section.dart +++ b/lib/core/widgets/shared_forms/attachments_section.dart @@ -153,7 +153,8 @@ class _SharedAttachmentsSectionState extends State { fileBytes = file.localBytes; } else if (file.storagePath != null && file.storagePath!.isNotEmpty) { fileBytes = await repository.downloadAttachmentBytes( - file.storagePath!, + storagePath: file.storagePath!, + bucket: Bucket.documents, ); } @@ -283,7 +284,8 @@ class _SharedAttachmentsSectionState extends State { fileBytes = file.localBytes; } else if (file.storagePath != null && file.storagePath!.isNotEmpty) { fileBytes = await repository.downloadAttachmentBytes( - file.storagePath!, + storagePath: file.storagePath!, + bucket: Bucket.documents, ); } diff --git a/lib/features/attachments/blocs/attachments_bloc.dart b/lib/features/attachments/blocs/attachments_bloc.dart index 80935fb..d081739 100644 --- a/lib/features/attachments/blocs/attachments_bloc.dart +++ b/lib/features/attachments/blocs/attachments_bloc.dart @@ -41,6 +41,7 @@ class AttachmentsBloc extends Bloc { add(LoadAttachmentsEvent(parentId: parentId)); } } + FutureOr _onParentEntitySaved( ParentEntitySavedEvent event, Emitter emit, @@ -67,6 +68,7 @@ class AttachmentsBloc extends Bloc { parentType: state.parentType, pickedFile: fakePlatformFile, companyId: companyId!, + bucket: _getBucketForParentType, ); }).toList(); @@ -156,6 +158,7 @@ class AttachmentsBloc extends Bloc { parentType: state.parentType, pickedFile: file, companyId: companyId!, + bucket: _getBucketForParentType, ); }).toList(); @@ -192,6 +195,7 @@ class AttachmentsBloc extends Bloc { parentType: state.parentType, pickedFile: file, companyId: event.companyId, + bucket: _getBucketForParentType, ), ); } @@ -218,6 +222,7 @@ class AttachmentsBloc extends Bloc { parentType: state.parentType, pickedFile: fakePlatformFile, companyId: event.companyId, + bucket: _getBucketForParentType, ), ); } @@ -242,6 +247,7 @@ class AttachmentsBloc extends Bloc { await _repository.deleteFiles( files: state.selectedFiles, currentContextType: state.parentType, + bucket: _getBucketForParentType, ); emit(state.copyWith(status: AttachmentsStatus.ready, selectedFiles: [])); } catch (e) { @@ -298,6 +304,8 @@ class AttachmentsBloc extends Bloc { return file.copyWith(ticketId: event.targetId); case AttachmentParentType.operation: return file.copyWith(operationId: event.targetId); + case AttachmentParentType.shippingDocument: + return file.copyWith(shippingDocumentId: event.targetId); } } return file; @@ -386,4 +394,17 @@ class AttachmentsBloc extends Bloc { emit(state.copyWith(localFiles: updatedLocalFiles)); } } + + Bucket get _getBucketForParentType { + switch (state.parentType) { + case AttachmentParentType.customer: + return Bucket.documents; + case AttachmentParentType.ticket: + return Bucket.documents; + case AttachmentParentType.operation: + return Bucket.documents; + case AttachmentParentType.shippingDocument: + return Bucket.companyDocuments; + } + } } diff --git a/lib/features/attachments/blocs/attachments_state.dart b/lib/features/attachments/blocs/attachments_state.dart index ae99107..b62115c 100644 --- a/lib/features/attachments/blocs/attachments_state.dart +++ b/lib/features/attachments/blocs/attachments_state.dart @@ -5,7 +5,8 @@ enum AttachmentsStatus { initial, loading, ready, uploading, success, failure } enum AttachmentParentType { operation('operation_id'), ticket('ticket_id'), - customer('customer_id'); + customer('customer_id'), + shippingDocument('shipping_document_id'); final String dbColumn; const AttachmentParentType(this.dbColumn); diff --git a/lib/features/attachments/data/attachments_repository.dart b/lib/features/attachments/data/attachments_repository.dart index 866ab30..7391463 100644 --- a/lib/features/attachments/data/attachments_repository.dart +++ b/lib/features/attachments/data/attachments_repository.dart @@ -4,17 +4,27 @@ import 'package:flux/features/attachments/models/attachment_model.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:flux/features/attachments/blocs/attachments_bloc.dart'; +enum Bucket { + documents('documents'), + companyDocuments('company_documents'); + + final String value; + const Bucket(this.value); +} + class AttachmentsRepository { final _supabase = Supabase.instance.client; - static const String _bucketName = 'documents'; static const String _tableName = 'attachment'; // Cambia col vero nome della tua tabella se diverso! /// Scarica i byte di un file direttamente da Supabase Storage - Future downloadAttachmentBytes(String storagePath) async { + Future downloadAttachmentBytes({ + required String storagePath, + required Bucket bucket, + }) async { try { final Uint8List bytes = await _supabase.storage - .from(_bucketName) + .from(bucket.value) .download(storagePath); return bytes; } catch (e) { @@ -31,6 +41,8 @@ class AttachmentsRepository { return 'ticket_id'; case AttachmentParentType.customer: return 'customer_id'; + case AttachmentParentType.shippingDocument: + return 'shipping_document_id'; } } @@ -55,41 +67,70 @@ class AttachmentsRepository { Future uploadAndRegisterFile({ required String parentId, required AttachmentParentType parentType, - required PlatformFile pickedFile, required String companyId, + required Bucket bucket, + PlatformFile? pickedFile, // Ora è opzionale + Uint8List? rawBytes, // Alternativa: bytes grezzi + String? rawFileName, // Alternativa: nome del file }) async { + // 🛡️ L'ASSERT NINJA: O c'è il pickedFile, o ci sono i byte e il nome. + // L'assert funziona solo in debug, ma è perfetto per beccare subito errori di chiamata! + assert( + pickedFile != null || (rawBytes != null && rawFileName != null), + 'Devi passare o un PlatformFile, oppure rawBytes e rawFileName!', + ); + try { - if (pickedFile.bytes == null) { - throw Exception( - "I bytes del file sono vuoti! Ricarica la pagina senza cache.", - ); + // 1. Normalizziamo i dati in base a cosa ci è stato passato + final Uint8List finalBytes; + final String finalFileName; + final int finalFileSize; + + if (pickedFile != null) { + if (pickedFile.bytes == null) { + throw Exception( + "I bytes del file sono vuoti! Ricarica la pagina senza cache.", + ); + } + finalBytes = pickedFile.bytes!; + finalFileName = pickedFile.name; + finalFileSize = pickedFile.size; + } else { + // Se pickedFile è null, grazie all'assert sappiamo che questi non lo sono + finalBytes = rawBytes!; + finalFileName = rawFileName!; + finalFileSize = finalBytes.length; // Calcoliamo la size dai byte reali } - final extension = pickedFile.extension ?? pickedFile.name.split('.').last; - final cleanName = pickedFile.name + // 2. Estraiamo l'estensione e puliamo il nome + final extension = finalFileName.contains('.') + ? finalFileName.split('.').last + : ''; // Fallback se il file non ha estensione + + final cleanName = finalFileName .replaceAll(RegExp(r'[^\w\s\.-]'), '') .replaceAll(' ', '_'); - // Creiamo un path ordinato: idAzienda/tipoEntita/idEntita/timestamp_nomefile + // 3. Creiamo un path ordinato: idAzienda/tipoEntita/idEntita/timestamp_nomefile final timestamp = DateTime.now().millisecondsSinceEpoch; final storagePath = '$companyId/${parentType.name}/$parentId/${timestamp}_$cleanName'; - // 1. Upload su Supabase Storage + // 4. Upload su Supabase Storage await _supabase.storage - .from(_bucketName) + .from(bucket.value) .uploadBinary( storagePath, - pickedFile.bytes!, + finalBytes, fileOptions: FileOptions(contentType: _guessContentType(extension)), ); - // 2. Creiamo la mappa per il DB dinamicamente + // 5. Creiamo la mappa per il DB dinamicamente final Map insertData = { 'company_id': companyId, - 'name': pickedFile.name.replaceAll('.$extension', ''), + 'name': finalFileName.replaceAll('.$extension', ''), 'extension': extension, - 'file_size': pickedFile.size, + 'file_size': finalFileSize, 'storage_path': storagePath, }; @@ -97,7 +138,7 @@ class AttachmentsRepository { final columnName = _getColumnNameForParent(parentType); insertData[columnName] = parentId; - // 3. Salviamo su Postgres + // 6. Salviamo su Postgres await _supabase.from(_tableName).insert(insertData); } catch (e) { throw Exception("Errore caricamento: $e"); @@ -108,6 +149,7 @@ class AttachmentsRepository { Future deleteFiles({ required List files, required AttachmentParentType currentContextType, + required Bucket bucket, }) async { if (files.isEmpty) return; @@ -120,6 +162,7 @@ class AttachmentsRepository { AttachmentParentType.operation: file.operationId, AttachmentParentType.ticket: file.ticketId, AttachmentParentType.customer: file.customerId, + AttachmentParentType.shippingDocument: file.shippingDocumentId, }; // 2. Simuliamo la rimozione del collegamento per il contesto attuale @@ -142,7 +185,7 @@ class AttachmentsRepository { await _supabase.from(_tableName).delete().eq('id', file.id!); if (file.storagePath != null) { - await _supabase.storage.from(_bucketName).remove([ + await _supabase.storage.from(bucket.value).remove([ file.storagePath!, ]); } diff --git a/lib/features/attachments/models/attachment_model.dart b/lib/features/attachments/models/attachment_model.dart index a1e0eb2..35de452 100644 --- a/lib/features/attachments/models/attachment_model.dart +++ b/lib/features/attachments/models/attachment_model.dart @@ -8,6 +8,7 @@ class AttachmentModel extends Equatable { final String? customerId; final String? operationId; final String? ticketId; + final String? shippingDocumentId; final String name; final String extension; final String? storagePath; @@ -21,6 +22,7 @@ class AttachmentModel extends Equatable { this.customerId, this.operationId, this.ticketId, + this.shippingDocumentId, required this.name, required this.extension, this.storagePath, @@ -36,6 +38,7 @@ class AttachmentModel extends Equatable { customerId, operationId, ticketId, + shippingDocumentId, name, extension, storagePath, @@ -63,6 +66,7 @@ class AttachmentModel extends Equatable { String? customerId, String? operationId, String? ticketId, + String? shippingDocumentId, String? name, String? extension, String? storagePath, @@ -75,6 +79,7 @@ class AttachmentModel extends Equatable { customerId: customerId ?? this.customerId, operationId: operationId ?? this.operationId, ticketId: ticketId ?? this.ticketId, + shippingDocumentId: shippingDocumentId ?? this.shippingDocumentId, name: name ?? this.name, extension: extension ?? this.extension, storagePath: storagePath ?? this.storagePath, @@ -92,6 +97,7 @@ class AttachmentModel extends Equatable { customerId: map['customer_id'] as String?, operationId: map['operation_id'] as String?, ticketId: map['ticket_id'] as String?, + shippingDocumentId: map['shipping_document_id'] as String?, name: map['name'] as String, extension: map['extension'] as String, storagePath: map['storage_path'] as String?, @@ -111,6 +117,7 @@ class AttachmentModel extends Equatable { 'customer_id': customerId, 'operation_id': operationId, 'ticket_id': ticketId, + 'shipping_document_id': shippingDocumentId, 'file_size': fileSize, 'company_id': companyId, }; diff --git a/lib/features/tickets/blocs/ticket_shipping_cubit.dart b/lib/features/tickets/blocs/ticket_shipping_cubit.dart index ca8a9c0..4c53b4b 100644 --- a/lib/features/tickets/blocs/ticket_shipping_cubit.dart +++ b/lib/features/tickets/blocs/ticket_shipping_cubit.dart @@ -1,9 +1,8 @@ - import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; -import 'package:flux/features/tickets/data/tickets_shipment_repository.dart'; -import 'package:flux/features/tickets/models/shipment_document_model.dart'; +import 'package:flux/features/tickets/data/tickets_shipping_repository.dart'; +import 'package:flux/features/tickets/models/shipping_document_model.dart'; import 'package:flux/features/master_data/providers/models/provider_location_model.dart'; import 'package:flux/features/master_data/providers/models/provider_model.dart'; import 'package:flux/features/settings/document_sequence/data/document_sequence_repository.dart'; @@ -16,15 +15,15 @@ import 'package:printing/printing.dart'; part 'ticket_shipping_state.dart'; class TicketShippingCubit extends Cubit { - final TicketsShipmentRepository _repository = - GetIt.I(); + final TicketsShippingRepository _repository = + GetIt.I(); final DocumentSequenceRepository _sequenceRepository = GetIt.I(); TicketShippingCubit({required List tickets}) : super( TicketShippingState( // Inizializziamo il modello direttamente nello stato! - document: ShipmentDocumentModel( + document: ShippingDocumentModel( companyId: GetIt.I.get().state.company!.id!, ticketIds: tickets.map((t) => t.id!).toList(), providerId: '', // Sarà riempito alla selezione diff --git a/lib/features/tickets/blocs/ticket_shipping_state.dart b/lib/features/tickets/blocs/ticket_shipping_state.dart index 513b621..b69987f 100644 --- a/lib/features/tickets/blocs/ticket_shipping_state.dart +++ b/lib/features/tickets/blocs/ticket_shipping_state.dart @@ -4,7 +4,7 @@ enum TicketShippingStatus { initial, loading, success, failure } class TicketShippingState extends Equatable { final TicketShippingStatus status; - final ShipmentDocumentModel document; + final ShippingDocumentModel document; final List tickets; // Dati di supporto per la UI @@ -25,7 +25,7 @@ class TicketShippingState extends Equatable { TicketShippingState copyWith({ TicketShippingStatus? status, - ShipmentDocumentModel? document, + ShippingDocumentModel? document, List? availableProviders, List? availableLocations, bool? isAutoNumber, diff --git a/lib/features/tickets/data/ticket_repository.dart b/lib/features/tickets/data/ticket_repository.dart index 7081181..ed0913c 100644 --- a/lib/features/tickets/data/ticket_repository.dart +++ b/lib/features/tickets/data/ticket_repository.dart @@ -31,7 +31,7 @@ class TicketRepository { .select(''' *, customer (*), - shipment_document (*), + shipment_document (*, attachments (*)), -- BAM! Deep Join created_by:staff_member!ticket_staff_id_fkey (*), assigned_to:staff_member!ticket_assigned_to_id_fkey (*), target_model:model!ticket_model_id_1_fkey (*), @@ -89,7 +89,7 @@ class TicketRepository { .select(''' *, customer (*), - shipment_document (*), + shipment_document (*, attachments (*)), created_by:staff_member!ticket_staff_id_fkey (*), assigned_to:staff_member!ticket_assigned_to_id_fkey (*), target_model:model!ticket_model_id_1_fkey (*), @@ -188,7 +188,7 @@ class TicketRepository { source_model:model!ticket_model_id_2_fkey (*), created_by:staff_member!ticket_staff_id_fkey (*), assigned_to:staff_member!ticket_assigned_to_id_fkey (*), - shipment_document (*), + shipment_document (*, attachments (*)), ''') .eq('id', ticketId) .single(); diff --git a/lib/features/tickets/data/tickets_shipment_repository.dart b/lib/features/tickets/data/tickets_shipment_repository.dart deleted file mode 100644 index a78255c..0000000 --- a/lib/features/tickets/data/tickets_shipment_repository.dart +++ /dev/null @@ -1,87 +0,0 @@ -import 'dart:typed_data'; - -import 'package:flux/features/tickets/models/shipment_document_model.dart'; -import 'package:flux/features/master_data/providers/models/provider_model.dart'; -import 'package:flux/features/master_data/providers/models/provider_role.dart'; -import 'package:get_it/get_it.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; - -class TicketsShipmentRepository { - final _supabase = GetIt.I.get(); - - Future> fetchRepairCenters() async { - try { - final response = await _supabase - .from('provider') - .select('*, provider_locations (*)') - .eq('is_active', true) - .order('name'); - - final allProviders = (response as List) - .map((row) => ProviderModel.fromMap(row as Map)) - .toList(); - - // Filtriamo lato client per prendere SOLO i repairCenter - return allProviders - .where((p) => p.roles.contains(ProviderRole.repairCenter)) - .toList(); - } catch (e) { - throw ('Errore caricamento laboratori: $e'); - } - } - - /// Salva il DDT nel DB, fa l'upload del PDF nello Storage e aggiorna il path - Future createShipmentWithPdf({ - required ShipmentDocumentModel document, - required Uint8List pdfBytes, - required String newTicketStatus, - }) async { - try { - // 1. Definiamo un percorso unico e ordinato per il file nello Storage - // Struttura: company_id / ddt / anno / numero_ddt.pdf - final year = document.docDate.year; - final cleanedDocNumber = document.docNumber - .replaceAll('/', '_') - .replaceAll(' ', '_'); - final storagePath = - '${document.companyId}/ddt/$year/$cleanedDocNumber.pdf'; - - // 2. Facciamo l'upload dei bytes grezzi nel bucket 'company_documents' - await _supabase.storage - .from('company_documents') - .uploadBinary( - storagePath, - pdfBytes, - fileOptions: const FileOptions( - contentType: 'application/pdf', - upsert: true, - ), - ); - - // 3. Creiamo la mappa del documento includendo il percorso dello storage - final documentData = document.toMap(); - documentData['storage_path'] = storagePath; - - // 4. Inseriamo il Documento di Trasporto nel DB - final savedDocument = await _supabase - .from('shipment_documents') - .insert(documentData) - .select('id') - .single(); - final documentid = savedDocument['id']; - // 5. Aggiorniamo lo stato di TUTTI i ticket inclusi nel DDT - await _supabase - .from('ticket') - .update({ - 'ticket_status': newTicketStatus, - 'shipment_document_id': documentid, - }) - .inFilter('id', document.ticketIds); - - // Restituiamo lo storagePath per usarlo subito nell'interfaccia se serve - return storagePath; - } catch (e) { - throw ('Errore durante il salvataggio e upload della spedizione: $e'); - } - } -} diff --git a/lib/features/tickets/data/tickets_shipping_repository.dart b/lib/features/tickets/data/tickets_shipping_repository.dart new file mode 100644 index 0000000..51ef640 --- /dev/null +++ b/lib/features/tickets/data/tickets_shipping_repository.dart @@ -0,0 +1,135 @@ +import 'dart:typed_data'; + +import 'package:flux/core/blocs/session/session_cubit.dart'; +import 'package:flux/features/attachments/blocs/attachments_bloc.dart'; +import 'package:flux/features/attachments/data/attachments_repository.dart'; +import 'package:flux/features/tickets/models/shipping_document_model.dart'; +import 'package:flux/features/master_data/providers/models/provider_model.dart'; +import 'package:flux/features/master_data/providers/models/provider_role.dart'; +import 'package:get_it/get_it.dart'; +import 'package:supabase_flutter/supabase_flutter.dart' hide Bucket; + +class TicketsShippingRepository { + final _supabase = GetIt.I.get(); + final _companyId = GetIt.I.get().state.company!.id!; + + Future> fetchRepairCenters() async { + try { + final response = await _supabase + .from('provider') + .select('*, provider_locations (*)') + .eq('is_active', true) + .order('name'); + + final allProviders = (response as List) + .map((row) => ProviderModel.fromMap(row as Map)) + .toList(); + + // Filtriamo lato client per prendere SOLO i repairCenter + return allProviders + .where((p) => p.roles.contains(ProviderRole.repairCenter)) + .toList(); + } catch (e) { + throw ('Errore caricamento laboratori: $e'); + } + } + + Future> fetchShipmentDocumentsForTicket( + String ticketId, + ) async { + try { + final response = await _supabase + .from('shipping_documents') + .select('*, attachments (*)') + .contains('ticket_ids', [ticketId]); + + return (response as List) + .map( + (row) => ShippingDocumentModel.fromMap(row as Map), + ) + .toList(); + } catch (e) { + throw ('Errore caricamento documenti di trasporto: $e'); + } + } + + Future> fetchCompanyShipmentDocuments() async { + try { + final response = await _supabase + .from('shipping_documents') + .select('*, attachments (*)') + .eq('company_id', _companyId); + + return (response as List) + .map( + (row) => ShippingDocumentModel.fromMap(row as Map), + ) + .toList(); + } catch (e) { + throw ('Errore caricamento documenti di trasporto aziendali: $e'); + } + } + + Future fetchShipmentDocumentById( + String documentId, + ) async { + try { + final response = await _supabase + .from('shipping_documents') + .select('*, attachments (*)') + .eq('id', documentId) + .single(); + + return ShippingDocumentModel.fromMap(response); + } catch (e) { + throw ('Errore caricamento documento di trasporto: $e'); + } + } + + /// Salva il DDT nel DB, carica il PDF nello Storage e lo registra come Attachment ufficiale + Future createShipmentWithPdf({ + required ShippingDocumentModel document, + required Uint8List pdfBytes, + required String newTicketStatus, + }) async { + try { + final attachmentsRepo = GetIt.I.get(); + + // 1. Inseriamo prima il Documento di Trasporto (DDT) su Postgres + // Il modello iniziale non ha ancora gli allegati popolati, quindi passiamo il toMap standard + final savedDocument = await _supabase + .from('shipping_documents') + .insert(document.toMap()) + .select('id') + .single(); + + final documentId = savedDocument['id'] as String; + + // 2. Prepariamo il nome del file PDF (es: DDT_123_2026.pdf) + final fileName = + 'DDT_${document.docNumber.replaceAll('/', '_').replaceAll(' ', '_')}.pdf'; + + // 3. IL TOCCO MASTER: Delegiamo l'upload e la registrazione nel DB all'AttachmentsRepository + // Questo creerà la riga nella tabella degli allegati agganciando lo 'shipping_document_id' + await attachmentsRepo.uploadAndRegisterFile( + parentId: documentId, + parentType: AttachmentParentType.shippingDocument, // Il tuo nuovo enum! + companyId: document.companyId, + bucket: Bucket.companyDocuments, + rawBytes: pdfBytes, + rawFileName: fileName, + ); + + // 4. Aggiorniamo lo stato di TUTTI i ticket inclusi nel DDT e li colleghiamo all'ID del DDT + await _supabase + .from('ticket') + .update({ + 'ticket_status': newTicketStatus, + 'shipment_document_id': documentId, + }) + .inFilter('id', document.ticketIds); + } catch (e) { + throw ('Errore durante il salvataggio e upload della spedizione con allegato: $e'); + } + } +} diff --git a/lib/features/tickets/models/shipment_document_model.dart b/lib/features/tickets/models/shipping_document_model.dart similarity index 73% rename from lib/features/tickets/models/shipment_document_model.dart rename to lib/features/tickets/models/shipping_document_model.dart index c9b2905..f0f0b96 100644 --- a/lib/features/tickets/models/shipment_document_model.dart +++ b/lib/features/tickets/models/shipping_document_model.dart @@ -1,6 +1,7 @@ import 'package:equatable/equatable.dart'; +import 'package:flux/features/attachments/models/attachment_model.dart'; -class ShipmentDocumentModel extends Equatable { +class ShippingDocumentModel extends Equatable { final String? id; final String companyId; final List ticketIds; @@ -12,8 +13,9 @@ class ShipmentDocumentModel extends Equatable { final double? weight; final String shippingReason; final String? notes; + final List attachments; - const ShipmentDocumentModel({ + const ShippingDocumentModel({ this.id, required this.companyId, required this.ticketIds, @@ -25,9 +27,10 @@ class ShipmentDocumentModel extends Equatable { this.weight, this.shippingReason = 'Riparazione esterna', this.notes, + this.attachments = const [], }); - ShipmentDocumentModel copyWith({ + ShippingDocumentModel copyWith({ String? id, String? companyId, List? ticketIds, @@ -39,8 +42,9 @@ class ShipmentDocumentModel extends Equatable { double? weight, String? shippingReason, String? notes, + List? attachments, }) { - return ShipmentDocumentModel( + return ShippingDocumentModel( id: id ?? this.id, companyId: companyId ?? this.companyId, ticketIds: ticketIds ?? this.ticketIds, @@ -53,11 +57,12 @@ class ShipmentDocumentModel extends Equatable { weight: weight ?? this.weight, shippingReason: shippingReason ?? this.shippingReason, notes: notes ?? this.notes, + attachments: attachments ?? this.attachments, ); } - factory ShipmentDocumentModel.fromMap(Map map) { - return ShipmentDocumentModel( + factory ShippingDocumentModel.fromMap(Map map) { + return ShippingDocumentModel( id: map['id'], companyId: map['company_id'], ticketIds: List.from(map['ticket_ids']), @@ -69,6 +74,11 @@ class ShipmentDocumentModel extends Equatable { weight: map['weight'] != null ? (map['weight'] as num).toDouble() : null, shippingReason: map['shipping_reason'] ?? 'Riparazione esterna', notes: map['notes'], + attachments: + (map['attachments'] as List?) + ?.map((e) => AttachmentModel.fromMap(e)) + .toList() ?? + const [], ); } @@ -89,5 +99,17 @@ class ShipmentDocumentModel extends Equatable { } @override - List get props => [id, docNumber, ticketIds]; + List get props => [ + id, + docNumber, + ticketIds, + providerId, + destinationLocationId, + docDate, + packageCount, + weight, + shippingReason, + notes, + attachments, + ]; } diff --git a/lib/features/tickets/models/ticket_model.dart b/lib/features/tickets/models/ticket_model.dart index 19f38b4..27f468a 100644 --- a/lib/features/tickets/models/ticket_model.dart +++ b/lib/features/tickets/models/ticket_model.dart @@ -1,7 +1,7 @@ import 'package:equatable/equatable.dart'; import 'package:flux/core/utils/extensions.dart'; import 'package:flux/features/customers/models/customer_model.dart'; -import 'package:flux/features/tickets/models/shipment_document_model.dart'; +import 'package:flux/features/tickets/models/shipping_document_model.dart'; /// Enum per il tipo di ticket enum TicketType { @@ -121,8 +121,7 @@ class TicketModel extends Equatable { final String? assignedToId; final String? assignedToName; final String? includedAccessories; - final ShipmentDocumentModel? - shippingDocument; // Per tenere in memoria i dati del DDT associato al ticket + final ShippingDocumentModel? shippingDocument; const TicketModel({ this.id, @@ -215,7 +214,7 @@ class TicketModel extends Equatable { String? assignedToId, String? assignedToName, String? includedAccessories, - ShipmentDocumentModel? shippingDocument, + ShippingDocumentModel? shippingDocument, }) { return TicketModel( id: id ?? this.id, @@ -312,7 +311,7 @@ class TicketModel extends Equatable { assignedToName: (map['assigned_to']?['name'] as String?)?.myFormat(), includedAccessories: map['included_accessories'] as String?, shippingDocument: map['shipping_document'] != null - ? ShipmentDocumentModel.fromMap( + ? ShippingDocumentModel.fromMap( map['shipping_document'] as Map, ) : null, diff --git a/lib/features/tickets/ui/widgets/ticket_list_card.dart b/lib/features/tickets/ui/widgets/ticket_list_card.dart index e3aeedf..5baaac9 100644 --- a/lib/features/tickets/ui/widgets/ticket_list_card.dart +++ b/lib/features/tickets/ui/widgets/ticket_list_card.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/routes/routes.dart'; +import 'package:flux/features/attachments/blocs/attachments_bloc.dart'; +import 'package:flux/features/attachments/models/attachment_model.dart'; +import 'package:flux/features/attachments/ui/attachment_viewer_screen.dart'; import 'package:flux/features/tickets/blocs/ticket_list_cubit.dart'; import 'package:flux/features/tickets/models/ticket_model.dart'; import 'package:flux/features/tickets/models/ticket_status_extension.dart'; @@ -20,6 +23,32 @@ class TicketListCard extends StatelessWidget { required this.isDesktop, }); + void _openFile({ + required BuildContext context, + required AttachmentModel file, + }) { + // 1. Catturiamo il BLoC dalla pagina corrente prima di navigare + final operationFilesBloc = context.read(); + Navigator.push( + context, + MaterialPageRoute( + builder: (viewerContext) => BlocProvider.value( + value: operationFilesBloc, + child: AttachmentViewerScreen( + attachment: file, + onRename: (newName) { + // Spara l'evento al BLoC e lui farà il resto! + operationFilesBloc.add(RenameAttachmentEvent(file, newName)); + }, + onDelete: () { + operationFilesBloc.add(DeleteSpecificAttachmentEvent(file)); + }, + ), + ), + ), + ); + } + @override Widget build(BuildContext context) { final statusColor = ticket.ticketStatus.color; @@ -110,21 +139,57 @@ class TicketListCard extends StatelessWidget { overflow: TextOverflow.ellipsis, ), ), - if (ticket.shippingDocument != null) ...[ - Row( - children: [ - const Text('DDT'), - const SizedBox(width: 4), - InkWell( - borderRadius: BorderRadius.circular(20), - onTap: () {}, - child: const Icon( - Icons.picture_as_pdf, - color: Colors.redAccent, - size: 20, - ), - ), - ], + if (ticket.shippingDocument != null && + ticket.shippingDocument!.attachments.isNotEmpty) ...[ + IconButton( + icon: const Icon( + Icons.picture_as_pdf, + color: Colors.redAccent, + ), + onPressed: () { + final attachments = + ticket.shippingDocument!.attachments; + + if (attachments.length == 1) { + // CASO 1: C'è solo il DDT. Apriamo SUBITO il Document Viewer! + _openFile( + context: context, + file: attachments.first, + ); + } else { + // CASO 2: Più allegati. Mostriamo una BottomSheet fulminea per far scegliere all'utente. + showModalBottomSheet( + context: context, + builder: (context) => ListView.builder( + shrinkWrap: true, + itemCount: attachments.length, + itemBuilder: (context, index) { + final file = attachments[index]; + return ListTile( + leading: Icon( + file.extension == 'pdf' + ? Icons.picture_as_pdf + : Icons.image, + ), + title: Text(file.name), + subtitle: Text( + '${(file.fileSize / 1024).toStringAsFixed(1)} KB', + ), + onTap: () { + Navigator.pop( + context, + ); // Chiude la scelta + _openFile( + context: context, + file: file, + ); // Apre il viewer + }, + ); + }, + ), + ); + } + }, ), ], diff --git a/lib/features/tickets/utils/ticket_shipping_pdf_service.dart b/lib/features/tickets/utils/ticket_shipping_pdf_service.dart index 3ebddef..893aa4c 100644 --- a/lib/features/tickets/utils/ticket_shipping_pdf_service.dart +++ b/lib/features/tickets/utils/ticket_shipping_pdf_service.dart @@ -1,6 +1,6 @@ import 'dart:typed_data'; import 'package:flux/features/company/models/company_model.dart'; -import 'package:flux/features/tickets/models/shipment_document_model.dart'; +import 'package:flux/features/tickets/models/shipping_document_model.dart'; import 'package:flux/features/master_data/providers/models/provider_location_model.dart'; import 'package:flux/features/master_data/providers/models/provider_model.dart'; import 'package:flux/features/tickets/models/ticket_model.dart'; @@ -13,7 +13,7 @@ class TicketShippingPdfService { required CompanyModel company, required ProviderModel provider, required ProviderLocationModel location, - required ShipmentDocumentModel document, + required ShippingDocumentModel document, required List tickets, }) async { final pdf = pw.Document(); diff --git a/lib/main.dart b/lib/main.dart index c53fb1e..397017e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,7 +7,7 @@ import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flux/features/attachments/data/attachments_repository.dart'; import 'package:flux/features/auth/bloc/auth_cubit.dart'; import 'package:flux/features/company/data/company_repository.dart'; -import 'package:flux/features/tickets/data/tickets_shipment_repository.dart'; +import 'package:flux/features/tickets/data/tickets_shipping_repository.dart'; import 'package:flux/features/master_data/providers/blocs/provider_list_cubit.dart'; import 'package:flux/features/operations/blocs/operation_list_cubit.dart'; import 'package:flux/features/operations/data/operations_repository.dart'; @@ -127,8 +127,8 @@ Future setupLocator() async { ); getIt.registerLazySingleton(() => CompanyRepository()); getIt.registerLazySingleton(() => TrackingRepository()); - getIt.registerLazySingleton( - () => TicketsShipmentRepository(), + getIt.registerLazySingleton( + () => TicketsShippingRepository(), ); }