diff --git a/lib/core/routes/app_router.dart b/lib/core/routes/app_router.dart index dc06c60..7a7efeb 100644 --- a/lib/core/routes/app_router.dart +++ b/lib/core/routes/app_router.dart @@ -131,9 +131,13 @@ class AppRouter { // Recuperiamo l'ID se presente nell'URL final serviceId = state.uri.queryParameters['serviceId']; - return ServiceFormScreen( - serviceId: serviceId ?? existingService?.id, - existingService: existingService, + return BlocProvider( + create: (context) => + ServiceFilesBloc(serviceId: serviceId ?? existingService?.id), + child: ServiceFormScreen( + serviceId: serviceId ?? existingService?.id, + existingService: existingService, + ), ); }, ), diff --git a/lib/features/services/blocs/service_files_bloc.dart b/lib/features/services/blocs/service_files_bloc.dart index d6f069e..85bf3c8 100644 --- a/lib/features/services/blocs/service_files_bloc.dart +++ b/lib/features/services/blocs/service_files_bloc.dart @@ -36,17 +36,34 @@ class ServiceFilesBloc extends Bloc { ServiceSavedEvent event, Emitter emit, ) { - emit(state.copyWith(serviceId: event.serviceId)); + // 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. + emit( + state.copyWith( + serviceId: event.serviceId, + localFiles: [], // <-- LA MAGIA ANTI-DUPLICATI + ), + ); + + // Lanciamo il caricamento + add(LoadServiceFilesEvent(serviceId: event.serviceId)); } FutureOr _onLoadServiceFiles( LoadServiceFilesEvent event, Emitter emit, ) async { - if (serviceId != null) { + // Usiamo l'ID dell'evento, e se non c'è usiamo quello dello stato + final currentId = event.serviceId ?? state.serviceId; + + if (currentId != null) { emit(state.copyWith(status: ServiceFilesStatus.loading)); + await emit.forEach( - _repository.getServiceFilesStream(serviceId!), + _repository.getServiceFilesStream( + currentId, + ), // <-- Usiamo l'ID corretto! onData: (data) => state.copyWith( status: ServiceFilesStatus.success, remoteFiles: data, diff --git a/lib/features/services/ui/service_form_screen/attachment_section.dart b/lib/features/services/ui/service_form_screen/attachment_section.dart index 6e09f97..1643421 100644 --- a/lib/features/services/ui/service_form_screen/attachment_section.dart +++ b/lib/features/services/ui/service_form_screen/attachment_section.dart @@ -5,6 +5,7 @@ import 'package:flux/core/blocs/session/session_cubit.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/customers/blocs/customer_files_bloc.dart'; import 'package:flux/features/services/blocs/service_files_bloc.dart'; import 'package:flux/features/services/blocs/services_cubit.dart'; import 'package:flux/features/services/models/service_file_model.dart'; @@ -32,207 +33,221 @@ class AttachmentsSection extends StatelessWidget { context, ); - return BlocBuilder( - builder: (context, state) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // --- HEADER SEZIONE --- - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "DOCUMENTI ALLEGATI", - style: TextStyle( - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.primary, - letterSpacing: 1.2, + return BlocListener( + listenWhen: (previous, current) => + previous.currentService?.id == null && + current.currentService?.id != null, + listener: (context, state) { + // FIGASSA! La pratica è stata salvata e ora ha un ID. + // Diciamo al Bloc dei file di agganciarsi al database. + final newId = state.currentService!.id!; + context.read().add(ServiceSavedEvent(newId)); + }, + child: BlocBuilder( + builder: (context, state) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // --- HEADER SEZIONE --- + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "DOCUMENTI ALLEGATI", + style: TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + letterSpacing: 1.2, + ), ), - ), - Row( - children: [ - OutlinedButton.icon( - icon: const Icon(Icons.attach_file), - label: const Text("Aggiungi File"), - onPressed: () => _pickFiles(context), - ), - if (!context.read().state.isMobileDevice) ...[ - const SizedBox(width: 12), - ElevatedButton.icon( - onPressed: () => _handleGenerateQr(context), - icon: const Icon(Icons.qr_code), - label: const Text("GENERA QR"), - style: ElevatedButton.styleFrom( - backgroundColor: Theme.of( - context, - ).colorScheme.primary.withValues(alpha: 0.1), - foregroundColor: Theme.of( - context, - ).colorScheme.primary, - elevation: 0, - ), + Row( + children: [ + OutlinedButton.icon( + icon: const Icon(Icons.attach_file), + label: const Text("Aggiungi File"), + onPressed: () => _pickFiles(context), ), - ], - ], - ), - ], - ), - const SizedBox(height: 12), - - // --- LISTA VUOTA --- - if (state.allFiles.isEmpty) // Furbata: usiamo allFiles.isEmpty! - Container( - width: double.infinity, - padding: const EdgeInsets.all(24), - decoration: BoxDecoration( - border: Border.all( - color: Colors.grey.shade300, - style: BorderStyle.solid, - ), - borderRadius: BorderRadius.circular(8), - color: Colors.grey.shade50, - ), - child: const Text( - "Nessun documento allegato alla bozza.", - textAlign: TextAlign.center, - style: TextStyle(color: Colors.grey), - ), - ) - // --- LISTA PIENA --- - else ...[ - ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: state.allFiles.length, - itemBuilder: (context, index) { - final file = state.allFiles[index]; - final sizeMb = (file.fileSize / (1024 * 1024)) - .toStringAsFixed(2); - final isPdf = file.extension.toLowerCase() == 'pdf'; - final isSelected = state.selectedFiles.contains(file); - - return GestureDetector( - onTap: () => serviceFilesBloc.add( - ToggleServiceFileSelectionEvent(file), - ), - onDoubleTap: () => _handleDoubleClick(context, file), - child: Card( - margin: const EdgeInsets.only(bottom: 8), - elevation: 0, - // UX Fina: cambiamo colore del bordo se selezionato - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - side: BorderSide( - color: isSelected - ? Theme.of(context).colorScheme.primary - : Colors.grey.shade300, - width: isSelected ? 2 : 1, - ), - ), - // UX Fina: Sfondo leggermente colorato se selezionato - color: isSelected - ? Theme.of( - context, - ).colorScheme.primary.withValues(alpha: 0.05) - : Theme.of(context).colorScheme.surface, - child: ListTile( - leading: Icon( - isSelected - ? Icons.check_box - : Icons.check_box_outline_blank, - color: Theme.of(context).colorScheme.primary, - size: 32, - ), - title: Text( - file.name, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - subtitle: Text( - file.isLocal ? "$sizeMb MB • (Nuovo)" : "$sizeMb MB", - ), - trailing: Icon( - isPdf ? Icons.picture_as_pdf : Icons.image, - color: isPdf ? Colors.red : Colors.blue, - size: 32, - ), - ), - ), - ); - }, - ), - - // --- PANNELLO AZIONI CONTESTUALI (LA MAGIA) --- - // Appare SOLO se c'è almeno un file selezionato - if (state.selectedFiles.isNotEmpty) - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - decoration: BoxDecoration( - color: Theme.of( - context, - ).colorScheme.primary.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: Theme.of( - context, - ).colorScheme.primary.withValues(alpha: 0.3), - ), - ), - child: Row( - children: [ - // Contatore - Text( - "${state.selectedFiles.length} file selezionati", - style: TextStyle( - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.primary, - ), - ), - const Spacer(), - - // Bottone Elimina - TextButton.icon( - style: TextButton.styleFrom( - foregroundColor: Colors.red, - ), - icon: const Icon(Icons.delete_outline), - label: const Text("Elimina"), - onPressed: () { - // Qui lancerai l'evento per eliminare i file selezionati! - // Es: serviceFilesBloc.add(DeleteSelectedFilesEvent()); - }, - ), - const SizedBox(width: 8), - - // Bottone Copia + if (!context + .read() + .state + .isMobileDevice) ...[ + const SizedBox(width: 12), ElevatedButton.icon( - icon: const Icon(Icons.copy), - label: const Text("Copia in Cliente"), - onPressed: () => saveAndCopyFilesToCustomer( - context, - state.selectedFiles, + onPressed: () => _handleGenerateQr(context), + icon: const Icon(Icons.qr_code), + label: const Text("GENERA QR"), + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of( + context, + ).colorScheme.primary.withValues(alpha: 0.1), + foregroundColor: Theme.of( + context, + ).colorScheme.primary, + elevation: 0, ), ), ], + ], + ), + ], + ), + const SizedBox(height: 12), + + // --- LISTA VUOTA --- + if (state.allFiles.isEmpty) // Furbata: usiamo allFiles.isEmpty! + Container( + width: double.infinity, + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + border: Border.all( + color: Colors.grey.shade300, + style: BorderStyle.solid, + ), + borderRadius: BorderRadius.circular(8), + color: Colors.grey.shade50, + ), + child: const Text( + "Nessun documento allegato alla bozza.", + textAlign: TextAlign.center, + style: TextStyle(color: Colors.grey), + ), + ) + // --- LISTA PIENA --- + else ...[ + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: state.allFiles.length, + itemBuilder: (context, index) { + final file = state.allFiles[index]; + final sizeMb = (file.fileSize / (1024 * 1024)) + .toStringAsFixed(2); + final isPdf = file.extension.toLowerCase() == 'pdf'; + final isSelected = state.selectedFiles.contains(file); + + return GestureDetector( + onTap: () => serviceFilesBloc.add( + ToggleServiceFileSelectionEvent(file), + ), + onDoubleTap: () => _handleDoubleClick(context, file), + child: Card( + margin: const EdgeInsets.only(bottom: 8), + elevation: 0, + // UX Fina: cambiamo colore del bordo se selezionato + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: BorderSide( + color: isSelected + ? Theme.of(context).colorScheme.primary + : Colors.grey.shade300, + width: isSelected ? 2 : 1, + ), + ), + // UX Fina: Sfondo leggermente colorato se selezionato + color: isSelected + ? Theme.of( + context, + ).colorScheme.primary.withValues(alpha: 0.05) + : Theme.of(context).colorScheme.surface, + child: ListTile( + leading: Icon( + isSelected + ? Icons.check_box + : Icons.check_box_outline_blank, + color: Theme.of(context).colorScheme.primary, + size: 32, + ), + title: Text( + file.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + subtitle: Text( + file.isLocal ? "$sizeMb MB • (Nuovo)" : " MB", + ), + trailing: Icon( + isPdf ? Icons.picture_as_pdf : Icons.image, + color: isPdf ? Colors.red : Colors.blue, + size: 32, + ), + ), + ), + ); + }, + ), + + // --- PANNELLO AZIONI CONTESTUALI (LA MAGIA) --- + // Appare SOLO se c'è almeno un file selezionato + if (state.selectedFiles.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + decoration: BoxDecoration( + color: Theme.of( + context, + ).colorScheme.primary.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: Theme.of( + context, + ).colorScheme.primary.withValues(alpha: 0.3), + ), + ), + child: Row( + children: [ + // Contatore + Text( + "${state.selectedFiles.length} file selezionati", + style: TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), + ), + const Spacer(), + + // Bottone Elimina + TextButton.icon( + style: TextButton.styleFrom( + foregroundColor: Colors.red, + ), + icon: const Icon(Icons.delete_outline), + label: const Text("Elimina"), + onPressed: () { + // Qui lancerai l'evento per eliminare i file selezionati! + // Es: serviceFilesBloc.add(DeleteSelectedFilesEvent()); + }, + ), + const SizedBox(width: 8), + + // Bottone Copia + ElevatedButton.icon( + icon: const Icon(Icons.copy), + label: const Text("Copia in Cliente"), + onPressed: () => saveAndCopyFilesToCustomer( + context, + state.selectedFiles, + ), + ), + ], + ), ), ), - ), + ], ], - ], - ); - }, + ); + }, + ), ); } Future _handleGenerateQr(BuildContext context) async { final cubit = context.read(); var currentService = cubit.state.currentService; - final Navigator = Navigator.of(context); + final navigator = Navigator.of(context); // 1. SE LA PRATICA E' NUOVA (Manca l'ID) if (currentService == null || currentService.id == null) { @@ -242,7 +257,7 @@ class AttachmentsSection extends StatelessWidget { builder: (ctx) => BlocListener( listener: (context, state) { if (state.status == ServiceFilesStatus.success) { - Navigator.of.context(ctx).pop(); + navigator.pop(); } }, child: AlertDialog(