import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flux/features/attachments/data/attachments_repository.dart'; import 'package:flux/features/attachments/ui/attachment_viewer_screen.dart'; import 'package:flux/features/attachments/ui/quick_rename_dialog.dart'; import 'package:flux/features/operations/blocs/operation_files_bloc.dart'; import 'package:get_it/get_it.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:flux/features/operations/models/operation_model.dart'; import 'package:flux/features/attachments/models/attachment_model.dart'; import 'package:pdf/widgets.dart' as pw; import 'package:pdf/pdf.dart' as p; // Se ti serve formattazione core import 'package:pdfx/pdfx.dart' as px; // Isoliamo pdfx class _ExportItem { final Uint8List bytes; final String sourceName; final bool isMultiPage; final int pageIndex; _ExportItem({ required this.bytes, required this.sourceName, required this.isMultiPage, required this.pageIndex, }); } class OperationFilesSection extends StatefulWidget { final OperationModel currentOp; const OperationFilesSection({super.key, required this.currentOp}); @override State createState() => _OperationFilesSectionState(); } class _OperationFilesSectionState extends State { String? _exportDirectory; @override void initState() { super.initState(); _loadExportDirectory(); } // --- GESTIONE CARTELLA CITRIX (SOLO DESKTOP) --- Future _loadExportDirectory() async { if (kIsWeb) return; final prefs = await SharedPreferences.getInstance(); setState(() { _exportDirectory = prefs.getString('citrix_export_path'); }); } Future _selectExportDirectory() async { final String? selectedDirectory = await FilePicker.getDirectoryPath( dialogTitle: 'Seleziona la cartella di esportazione per TIM/Citrix', ); if (selectedDirectory != null) { final prefs = await SharedPreferences.getInstance(); await prefs.setString('citrix_export_path', selectedDirectory); setState(() { _exportDirectory = selectedDirectory; }); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Cartella Export impostata: $selectedDirectory'), ), ); } } } // --- SELEZIONE FILE DAL PC/TELEFONO --- Future _pickFiles() async { final result = await FilePicker.pickFiles( allowMultiple: true, type: FileType.custom, allowedExtensions: ['jpg', 'jpeg', 'png', 'pdf'], withData: true, ); if (result != null && mounted) { // MAGIA: Passiamo direttamente la lista di PlatformFile al tuo BLoC! context.read().add( AddOperationFilesEvent(result.files), ); } } // --- APERTURA VIEWER --- void _openFile(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(RenameOperationFileEvent(file, newName)); }, onDelete: () { operationFilesBloc.add(DeleteSpecificOperationFileEvent(file)); }, ), ), ), ); } Future _exportMergedPdf(List selectedFiles) async { if (!kIsWeb && (_exportDirectory == null || _exportDirectory!.isEmpty)) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Imposta prima la cartella Citrix!')), ); return; } showDialog( context: context, barrierDismissible: false, builder: (_) => const Center(child: CircularProgressIndicator()), ); try { // 1. "FLATTEN" DI TUTTO (Stessa magia di prima) List allPagesAsImages = []; final repository = GetIt.I.get(); for (var file in selectedFiles) { Uint8List? fileBytes; if (file.localBytes != null) { fileBytes = file.localBytes; } else if (file.storagePath != null && file.storagePath!.isNotEmpty) { fileBytes = await repository.downloadAttachmentBytes( file.storagePath!, ); } if (fileBytes == null) continue; if (file.extension == 'pdf') { final document = await px.PdfDocument.openData(fileBytes); for (int i = 1; i <= document.pagesCount; i++) { final page = await document.getPage(i); final pageImage = await page.render( width: page.width * 2, height: page.height * 2, format: px.PdfPageImageFormat.jpeg, ); if (pageImage != null) { allPagesAsImages.add(pageImage.bytes); } await page.close(); } await document.close(); } else { // È un'immagine allPagesAsImages.add(fileBytes); } } if (mounted) Navigator.pop(context); // Togliamo il loading // Se per qualche motivo la lista è vuota, usciamo if (allPagesAsImages.isEmpty) return; // 2. LOGICA DEL NOME SUGGERITO String suggestedName; if (selectedFiles.length == 1) { // Se c'è un solo file (es. ho selezionato 3 foto ma poi ho deselezionato le altre) suggestedName = selectedFiles.first.name; } else { // Se sono più file uniti suggestedName = '${widget.currentOp.customerDisplayName}_Unito'; } if (!mounted) return; // 3. DIALOG DI CONFERMA (Mostriamo la PRIMA pagina come anteprima per fargli capire cos'è) final finalName = await showDialog( context: context, builder: (_) => QuickRenameDialog( suggestedName: suggestedName, previewWidget: Image.memory( allPagesAsImages.first, fit: BoxFit.contain, ), ), ); if (finalName == null || finalName.isEmpty) return; // Ha annullato // 4. CREAZIONE DEL PDF UNICO (IL MERGE VERO E PROPRIO) final pdf = pw.Document(); // Cicliamo su tutte le immagini estratte e creiamo una pagina per ognuna for (var imageBytes in allPagesAsImages) { final pdfImage = pw.MemoryImage(imageBytes); pdf.addPage( pw.Page( margin: pw.EdgeInsets.zero, build: (pw.Context context) { return pw.Center(child: pw.Image(pdfImage)); }, ), ); } final mergedPdfBytes = await pdf.save(); // 5. SALVATAGGIO SUL DISCO if (kIsWeb) { // Trigger download web } else { final fileToSave = File('$_exportDirectory/$finalName.pdf'); await fileToSave.writeAsBytes(mergedPdfBytes); } if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('PDF Multi-pagina creato e salvato con successo!'), ), ); } } catch (e) { if (mounted) { // Se il loading è ancora aperto, lo chiudiamo if (Navigator.canPop(context)) Navigator.pop(context); ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text('Errore durante l\'unione: $e'))); } } } Future _exportSplitPdfs(List selectedFiles) async { if (!kIsWeb && (_exportDirectory == null || _exportDirectory!.isEmpty)) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Imposta prima la cartella Citrix!')), ); return; } showDialog( context: context, barrierDismissible: false, builder: (_) => const Center(child: CircularProgressIndicator()), ); try { // 1. "FLATTEN" INTELLIGENTE (Ora usiamo la nostra classe _ExportItem) List<_ExportItem> itemsToExport = []; final repository = GetIt.I.get(); for (var file in selectedFiles) { Uint8List? fileBytes; if (file.localBytes != null) { fileBytes = file.localBytes; } else if (file.storagePath != null && file.storagePath!.isNotEmpty) { fileBytes = await repository.downloadAttachmentBytes( file.storagePath!, ); } if (fileBytes == null) continue; // Recuperiamo il nome che l'utente ha (magari) già impostato final baseName = file.name ?? 'Documento'; if (file.extension == 'pdf') { final document = await px.PdfDocument.openData(fileBytes); final isMulti = document.pagesCount > 1; // Controlliamo se è multipagina! for (int i = 1; i <= document.pagesCount; i++) { final page = await document.getPage(i); final pageImage = await page.render( width: page.width * 2, height: page.height * 2, format: px.PdfPageImageFormat.jpeg, ); if (pageImage != null) { // Salviamo l'immagine CON il suo contesto storico itemsToExport.add( _ExportItem( bytes: pageImage.bytes, sourceName: baseName, isMultiPage: isMulti, pageIndex: i, ), ); } await page.close(); } await document.close(); } else { // SE È UN'IMMAGINE, la salviamo come singola pagina itemsToExport.add( _ExportItem( bytes: fileBytes, sourceName: baseName, isMultiPage: false, pageIndex: 1, ), ); } } if (mounted) Navigator.pop(context); // 2. IL CICLO UX for (var item in itemsToExport) { if (!mounted) return; // LA TUA MAGIA UX SUI NOMI: // Se è singolo (foto o PDF da 1 pag) -> Usa il nome originale nudo e crudo! // Se è multipagina -> Usa il nome originale + il numero di pagina String suggestedName = item.sourceName; if (item.isMultiPage) { suggestedName = '${item.sourceName}_Pag_${item.pageIndex}'; } final finalName = await showDialog( context: context, builder: (_) => QuickRenameDialog( suggestedName: suggestedName, previewWidget: Image.memory(item.bytes, fit: BoxFit.contain), ), ); if (finalName == null || finalName.isEmpty) continue; // CREAZIONE DEL PDF SINGOLO final pdf = pw.Document(); final pdfImage = pw.MemoryImage(item.bytes); // Usiamo item.bytes! pdf.addPage( pw.Page( margin: pw.EdgeInsets.zero, build: (pw.Context context) { return pw.Center(child: pw.Image(pdfImage)); }, ), ); final singlePdfBytes = await pdf.save(); if (kIsWeb) { // Trigger download web } else { final fileToSave = File('$_exportDirectory/$finalName.pdf'); await fileToSave.writeAsBytes(singlePdfBytes); } } if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Esportazione completata con successo!'), ), ); } } catch (e) { if (mounted) { Navigator.pop(context); ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text('Errore: $e'))); } } } @override Widget build(BuildContext context) { final theme = Theme.of(context); // USIAMO IL TUO BLOC! return BlocBuilder( builder: (context, state) { final allFiles = state.allFiles; final selectedFiles = state.selectedFiles; final hasSelection = selectedFiles.isNotEmpty; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 1. SETTINGS CARTELLA (Solo visibile su Desktop) if (!kIsWeb) Card( color: theme.colorScheme.surfaceContainerHighest.withValues( alpha: 0.5, ), elevation: 0, margin: const EdgeInsets.only(bottom: 16), child: ListTile( leading: Icon( Icons.folder_special, color: theme.colorScheme.primary, ), title: const Text( 'Cartella Export (Es. Citrix TIM)', style: TextStyle(fontWeight: FontWeight.bold), ), subtitle: Text( _exportDirectory ?? 'Nessuna cartella selezionata. Clicca per impostare.', style: TextStyle( color: _exportDirectory == null ? theme.colorScheme.error : null, ), ), trailing: const Icon(Icons.settings), onTap: _selectExportDirectory, ), ), // 2. ACTION BAR DINAMICA Wrap( crossAxisAlignment: WrapCrossAlignment.center, spacing: 8, runSpacing: 8, children: [ // Bottone di Aggiunta ElevatedButton.icon( icon: const Icon(Icons.add_photo_alternate), label: const Text('Aggiungi File'), onPressed: state.status == OperationFilesStatus.uploading ? null : _pickFiles, ), const SizedBox(width: 12), // NUOVO: SELEZIONA / DESELEZIONA TUTTO if (allFiles.isNotEmpty) ...[ TextButton.icon( icon: Icon( selectedFiles.length == allFiles.length ? Icons.deselect : Icons.select_all, ), label: Text( selectedFiles.length == allFiles.length ? 'Deseleziona Tutto' : 'Seleziona Tutto', ), onPressed: () { if (selectedFiles.length == allFiles.length) { context.read().add( ClearOperationFileSelectionEvent(), ); } else { context.read().add( SelectAllOperationFilesEvent(), ); } }, ), ], const SizedBox(width: 12), // Loader di upload if (state.status == OperationFilesStatus.uploading) const SizedBox( width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2), ), const Spacer(), // Azioni visibili SOLO se c'è una selezione! if (hasSelection) ...[ // Bottone Elimina IconButton( icon: const Icon(Icons.delete, color: Colors.red), tooltip: 'Elimina selezionati', onPressed: () { context.read().add( DeleteOperationFilesEvent(), ); }, ), // Bottone Associa a Cliente if (widget.currentOp.customerId != null && widget.currentOp.customerId!.isNotEmpty) IconButton( icon: const Icon(Icons.person_add, color: Colors.blue), tooltip: 'Copia nei documenti del Cliente', onPressed: () { context.read().add( LinkFilesToCustomerEvent( customerId: widget.currentOp.customerId!, ), ); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('File copiati nella scheda cliente!'), ), ); }, ), // IL NUOVO BOTTONE ESPORTA CON MENU A TENDINA PopupMenuButton( tooltip: 'Opzioni di esportazione', position: PopupMenuPosition .under, // Opzionale: fa aprire il menu sotto al bottone onSelected: (value) { if (value == 'merge') { _exportMergedPdf(selectedFiles); } else if (value == 'split') { _exportSplitPdfs(selectedFiles); } }, itemBuilder: (BuildContext context) => >[ const PopupMenuItem( value: 'merge', child: ListTile( leading: Icon( Icons.merge_type, color: Colors.blue, ), title: Text('Unisci in un singolo PDF'), ), ), const PopupMenuItem( value: 'split', child: ListTile( leading: Icon( Icons.splitscreen, color: Colors.orange, ), title: Text( 'Dividi: un PDF per ogni pagina/foto', ), ), ), ], // IL FIX È QUI SOTTO: AbsorbPointer + onPressed vuoto child: AbsorbPointer( child: FilledButton.icon( style: FilledButton.styleFrom( backgroundColor: Colors.red, ), icon: const Icon(Icons.picture_as_pdf), label: Text('Esporta (${selectedFiles.length})'), onPressed: () {}, // Manteniamo vivo il colore! ), ), ), ], ], ), const SizedBox(height: 16), // 3. GRIGLIA DEI FILE if (allFiles.isEmpty) Container( width: double.infinity, padding: const EdgeInsets.all(32), decoration: BoxDecoration( border: Border.all( color: theme.dividerColor, style: BorderStyle.solid, ), borderRadius: BorderRadius.circular(8), ), child: const Column( children: [ Icon(Icons.upload_file, size: 48, color: Colors.grey), SizedBox(height: 8), Text( 'Nessun file allegato. Usa il pulsante per aggiungere documenti o foto.', textAlign: TextAlign.center, style: TextStyle(color: Colors.grey), ), ], ), ) else GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 150, crossAxisSpacing: 12, mainAxisSpacing: 12, childAspectRatio: 0.8, ), itemCount: allFiles.length, itemBuilder: (context, index) { final file = allFiles[index]; final isPdf = file.extension == 'pdf'; final isSelected = selectedFiles.contains(file); final isLocal = file.localBytes != null; // Per capire se è un file in bozza return Stack( children: [ // CARD DEL FILE InkWell( onTap: () => _openFile(file), onLongPress: () { // Selezione rapida con long press! context.read().add( ToggleOperationFileSelectionEvent(file), ); }, borderRadius: BorderRadius.circular(8), child: Container( decoration: BoxDecoration( border: Border.all( color: isSelected ? theme.colorScheme.primary : theme.dividerColor, width: isSelected ? 3 : 1, ), borderRadius: BorderRadius.circular(8), ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Anteprima Expanded( child: Container( decoration: BoxDecoration( color: theme .colorScheme .surfaceContainerHighest, borderRadius: const BorderRadius.vertical( top: Radius.circular(8), ), ), child: isPdf ? const Icon( Icons.picture_as_pdf, size: 48, color: Colors.red, ) : isLocal ? ClipRRect( borderRadius: const BorderRadius.vertical( top: Radius.circular(8), ), child: Image.memory( file.localBytes!, fit: BoxFit.cover, ), ) : const Icon( Icons.image, size: 48, color: Colors.blue, ), // Da remoto metterai il tuo NetworkImage se vuoi ), ), // Nome File Padding( padding: const EdgeInsets.all(8.0), child: Text( file.name, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 12), textAlign: TextAlign.center, ), ), ], ), ), ), // CHECKBOX DI SELEZIONE Positioned( top: 4, right: 4, child: InkWell( onTap: () { context.read().add( ToggleOperationFileSelectionEvent(file), ); }, child: Container( decoration: BoxDecoration( color: isSelected ? theme.colorScheme.primary : Colors.white.withValues(alpha: 0.8), shape: BoxShape.circle, border: Border.all( color: theme.colorScheme.primary, ), ), child: Padding( padding: const EdgeInsets.all(4.0), child: Icon( isSelected ? Icons.check : Icons.circle, size: 16, color: isSelected ? Colors.white : Colors.transparent, ), ), ), ), ), // BADGE "IN ATTESA" (Se è locale ma la pratica è salvata) if (isLocal) Positioned( top: 4, left: 4, child: Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2, ), decoration: BoxDecoration( color: Colors.orange, borderRadius: BorderRadius.circular(4), ), child: const Text( 'Bozza', style: TextStyle( color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold, ), ), ), ), ], ); }, ), ], ); }, ); } }