diff --git a/lib/core/routes/app_router.dart b/lib/core/routes/app_router.dart index 0160e15..ff8160b 100644 --- a/lib/core/routes/app_router.dart +++ b/lib/core/routes/app_router.dart @@ -6,12 +6,11 @@ import 'package:flux/core/data/core_repository.dart'; import 'package:flux/core/layout/app_shell.dart'; import 'package:flux/core/utils/extensions.dart'; import 'package:flux/core/widgets/set_password_screen.dart'; +import 'package:flux/core/widgets/shared_forms/shared_mobile_upload_screen.dart'; import 'package:flux/features/auth/ui/auth_screen.dart'; -import 'package:flux/features/customers/blocs/customer_files_bloc.dart'; import 'package:flux/features/customers/blocs/customers_cubit.dart'; import 'package:flux/features/customers/models/customer_model.dart'; import 'package:flux/features/customers/ui/customer_detail_screen.dart'; -import 'package:flux/features/customers/ui/customer_mobile_upload_screen.dart'; import 'package:flux/features/customers/ui/customers_content.dart'; import 'package:flux/features/home/ui/home_screen.dart'; import 'package:flux/features/master_data/master_data_hub_content.dart'; @@ -27,7 +26,6 @@ import 'package:flux/features/onboarding/ui/onboarding_screen.dart'; import 'package:flux/features/attachments/blocs/attachments_bloc.dart'; import 'package:flux/features/operations/models/operation_model.dart'; import 'package:flux/features/operations/ui/operation_form_screen.dart'; -import 'package:flux/features/operations/ui/operation_mobile_upload_screen.dart'; import 'package:flux/features/operations/ui/operations_screen.dart'; import 'package:flux/features/tickets/blocs/ticket_list_cubit.dart'; import 'package:flux/features/tickets/ui/ticket_list_screen.dart'; @@ -164,7 +162,10 @@ class AppRouter { builder: (context, state) { final customer = state.extra as CustomerModel; return BlocProvider( - create: (context) => CustomerFilesBloc(customer.id!), + create: (context) => AttachmentsBloc( + parentType: AttachmentParentType.customer, + parentId: customer.id, + ), child: CustomerDetailScreen(customer: customer), ); }, @@ -175,10 +176,12 @@ class AppRouter { final customerId = state.pathParameters['id']!; final customerName = state.uri.queryParameters['name'] ?? 'Cliente'; return BlocProvider( - create: (context) => CustomerFilesBloc(customerId), - child: CustomerMobileUploadScreen( - customerId: customerId, - customerName: customerName, + create: (context) => AttachmentsBloc( + parentType: AttachmentParentType.customer, + parentId: customerId, + ), + child: SharedMobileUploadScreen( + title: 'Aggiungi allegati al cliente $customerName', ), ); }, @@ -237,9 +240,8 @@ class AppRouter { parentId: operationId, parentType: AttachmentParentType.operation, ), - child: OperationMobileUploadScreen( - operationId: operationId, - operationName: operationName, + child: SharedMobileUploadScreen( + title: 'Aggiungi allegati alla pratica $operationName', ), ); }, diff --git a/lib/features/operations/ui/operation_mobile_upload_screen.dart b/lib/core/widgets/shared_forms/shared_mobile_upload_screen.dart similarity index 86% rename from lib/features/operations/ui/operation_mobile_upload_screen.dart rename to lib/core/widgets/shared_forms/shared_mobile_upload_screen.dart index 2ff950c..e1c6b3d 100644 --- a/lib/features/operations/ui/operation_mobile_upload_screen.dart +++ b/lib/core/widgets/shared_forms/shared_mobile_upload_screen.dart @@ -1,27 +1,21 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/features/attachments/blocs/attachments_bloc.dart'; import 'package:image_picker/image_picker.dart'; import 'package:file_picker/file_picker.dart'; +import 'package:flux/features/attachments/blocs/attachments_bloc.dart'; -class OperationMobileUploadScreen extends StatefulWidget { - final String operationId; - final String operationName; +class SharedMobileUploadScreen extends StatefulWidget { + final String title; - const OperationMobileUploadScreen({ - super.key, - required this.operationId, - required this.operationName, - }); + const SharedMobileUploadScreen({super.key, required this.title}); @override - State createState() => - _OperationMobileUploadScreenState(); + State createState() => + _SharedMobileUploadScreenState(); } -class _OperationMobileUploadScreenState - extends State { +class _SharedMobileUploadScreenState extends State { // 1. LA NOSTRA STAGING AREA (Il "Carrello") final List _stagedFiles = []; @@ -56,7 +50,8 @@ class _OperationMobileUploadScreenState }, child: Scaffold( appBar: AppBar( - title: Text("Upload Pratica:\n${widget.operationName}"), + title: Text("Upload: ${widget.title}"), + // Togliamo la freccia indietro se stiamo caricando per evitare disastri automaticallyImplyLeading: !_isUploading, ), body: Stack( @@ -109,8 +104,7 @@ class _OperationMobileUploadScreenState padding: const EdgeInsets.all(16), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: - 3, // 3 colonne come la galleria dell'iPhone + crossAxisCount: 3, // 3 colonne stile galleria crossAxisSpacing: 12, mainAxisSpacing: 12, ), @@ -136,10 +130,17 @@ class _OperationMobileUploadScreenState child: ClipRRect( borderRadius: BorderRadius.circular(12), child: isImg - ? Image.file( - File(file.path!), - fit: BoxFit.cover, - ) + ? (file.bytes != null + // Se abbiamo i bytes (es. scatto da fotocamera) usiamo quelli (a prova di Web!) + ? Image.memory( + file.bytes!, + fit: BoxFit.cover, + ) + // Altrimenti andiamo di file fisico + : Image.file( + File(file.path!), + fit: BoxFit.cover, + )) : const Column( mainAxisAlignment: MainAxisAlignment.center, @@ -227,9 +228,10 @@ class _OperationMobileUploadScreenState ], ), - // --- OVERLAY DI CARICAMENTO (Impedisce tap multipli) --- + // --- OVERLAY DI CARICAMENTO --- if (_isUploading) Container( + // Usa il metodo non deprecato che hai giustamente suggerito! color: Colors.black.withValues(alpha: 0.5), child: const Center( child: Card( @@ -264,7 +266,7 @@ class _OperationMobileUploadScreenState imageQuality: 80, ); if (photo != null) { - final photoBytes = await photo.readAsBytes(); // Sicuro anche per Web! + final photoBytes = await photo.readAsBytes(); final photoSize = await photo.length(); final platformFile = PlatformFile( @@ -274,13 +276,12 @@ class _OperationMobileUploadScreenState bytes: photoBytes, // I bytes ci salvano la vita su Supabase! ); setState(() { - _stagedFiles.add(platformFile); // Unifichiamo tutto in un dart:io File + _stagedFiles.add(platformFile); }); } } Future _handleFilePicker() async { - // allowMultiple: true permette di pescare 5 foto dalla galleria in un colpo solo! final result = await FilePicker.pickFiles(allowMultiple: true); if (result != null) { setState(() { @@ -293,11 +294,9 @@ class _OperationMobileUploadScreenState void _submitAllFiles() { setState(() => _isUploading = true); - // 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(UploadAttachmentsEvent(pickedFiles: _stagedFiles)); - - // N.B: Il Navigator.pop() viene chiamato dal BlocListener in alto quando lo stato diventa "success"! + // Lanciamo l'evento del nostro nuovo AttachmentsBloc Agnostico! + context.read().add( + UploadAttachmentsEvent(pickedFiles: _stagedFiles), + ); } } diff --git a/lib/core/widgets/shared_forms/shared_model_section.dart b/lib/core/widgets/shared_forms/shared_model_section.dart new file mode 100644 index 0000000..1bdc70f --- /dev/null +++ b/lib/core/widgets/shared_forms/shared_model_section.dart @@ -0,0 +1,165 @@ +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/master_data/products/ui/quick_product_dialog.dart'; + +class SharedModelSection extends StatelessWidget { + final String? modelId; + final String? modelName; + final String label; + + // Usiamo una callback che passa direttamente ID e Nome + // così non dobbiamo preoccuparci di importare la classe esatta del modello ovunque + final void Function(String id, String name) onModelSelected; + + const SharedModelSection({ + super.key, + required this.modelId, + required this.modelName, + required this.onModelSelected, + this.label = 'Seleziona Modello', + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final hasModel = modelId != null && modelId!.isNotEmpty; + + return ListTile( + title: Text(label), + subtitle: Text( + hasModel ? modelName! : 'Nessun modello selezionato', + style: TextStyle( + color: hasModel ? null : Colors.grey, + fontWeight: hasModel ? FontWeight.bold : FontWeight.normal, + ), + ), + trailing: const Icon(Icons.arrow_drop_down), + shape: RoundedRectangleBorder( + side: BorderSide(color: theme.dividerColor), + borderRadius: BorderRadius.circular(8), + ), + onTap: () => _showModelModal(context), + ); + } + + void _showModelModal(BuildContext context) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + builder: (modalContext) { + return DraggableScrollableSheet( + initialChildSize: 0.6, + minChildSize: 0.4, + maxChildSize: 0.9, + expand: false, + builder: (_, scrollController) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Seleziona Modello', + style: Theme.of(context).textTheme.titleLarge, + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.pop(modalContext), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: TextField( + decoration: InputDecoration( + hintText: 'Cerca modello (es. iPhone 15...)', + prefixIcon: const Icon(Icons.search), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onChanged: (query) => + context.read().searchModels(query), + ), + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton.icon( + style: ElevatedButton.styleFrom( + minimumSize: const Size.fromHeight(48), + ), + icon: const Icon(Icons.add), + label: const Text('Aggiungi Modello al Volo'), + onPressed: () async { + // Leggiamo i brand dal Cubit per passarli alla dialog + final existingBrands = context + .read() + .state + .brands; + + final newModel = await showDialog( + context: context, + builder: (dialogContext) { + return BlocProvider.value( + value: context.read(), + child: QuickProductDialog( + existingBrands: existingBrands, + ), + ); + }, + ); + + if (newModel != null) { + // CHIAMIAMO LA CALLBACK! + onModelSelected(newModel.id, newModel.nameWithBrand); + if (context.mounted) Navigator.pop(modalContext); + } + }, + ), + ), + const Divider(), + Expanded( + child: BlocBuilder( + builder: (context, state) { + return ListView.builder( + controller: scrollController, + itemCount: state.models.length, + itemBuilder: (context, index) { + final deviceModel = state.models[index]; + return ListTile( + leading: const Icon(Icons.devices), + title: Text( + deviceModel.nameWithBrand, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + onTap: () { + // CHIAMIAMO LA CALLBACK! + onModelSelected( + deviceModel.id!, + deviceModel.nameWithBrand, + ); + Navigator.pop(modalContext); + }, + ); + }, + ); + }, + ), + ), + ], + ); + }, + ); + }, + ); + } +} diff --git a/lib/features/customers/blocs/customer_files_bloc.dart b/lib/features/customers/blocs/customer_files_bloc.dart deleted file mode 100644 index c8662e0..0000000 --- a/lib/features/customers/blocs/customer_files_bloc.dart +++ /dev/null @@ -1,139 +0,0 @@ -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/features/attachments/models/attachment_model.dart'; -import 'package:flux/features/customers/data/customer_repository.dart'; -import 'package:get_it/get_it.dart'; - -part 'customer_files_events.dart'; -part 'customer_files_state.dart'; - -class CustomerFilesBloc extends Bloc { - final CustomerRepository _repository = GetIt.I(); - final String customerId; - CustomerFilesBloc(this.customerId) - : super(const CustomerFilesState(status: CustomerFilesStatus.initial)) { - on(_loadCustomerFiles); - on(_uploadCustomerFile); - on(_uploadMultipleCustomerFiles); - on(_deleteCustomerFiles); - on(_toggleCustomerFileSelection); - } - void _loadCustomerFiles( - LoadCustomerFilesEvent event, - Emitter emit, - ) async { - await emit.forEach>( - _repository.getCustomerFilesStream(customerId), - onData: (customerFiles) => CustomerFilesState( - status: CustomerFilesStatus.success, - customerFiles: customerFiles, - ), - onError: (error, stackTrace) => CustomerFilesState( - status: CustomerFilesStatus.failure, - error: error.toString(), - ), - ); - } - - Future _uploadCustomerFile( - UploadCustomerFileEvent event, - Emitter emit, - ) async { - emit(state.copyWith(status: CustomerFilesStatus.uploading)); - if (event.pickedFile != null) { - try { - await _repository.uploadAndRegisterFile( - customerId: customerId, - pickedFile: event.pickedFile!, - ); - emit(state.copyWith(status: CustomerFilesStatus.success)); - } catch (e) { - emit( - state.copyWith( - status: CustomerFilesStatus.failure, - error: e.toString(), - ), - ); - } - } - } - - FutureOr _uploadMultipleCustomerFiles( - UploadMultipleCustomerFilesEvent event, - Emitter emit, - ) async { - if (event.files.isEmpty) { - emit( - state.copyWith( - status: CustomerFilesStatus.failure, - error: "Nessun file selezionato", - ), - ); - return; - } - emit(state.copyWith(status: CustomerFilesStatus.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.uploadAndRegisterFile( - customerId: customerId, - 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: CustomerFilesStatus.success)); - } catch (e) { - // Se anche un solo file fallisce, catturiamo l'errore - emit( - state.copyWith( - status: CustomerFilesStatus.failure, - error: "Errore durante l'upload multiplo: $e", - ), - ); - } - } - - Future _deleteCustomerFiles( - DeleteCustomerFilesEvent event, - Emitter emit, - ) async { - emit(state.copyWith(status: CustomerFilesStatus.loading)); - try { - await _repository.deleteDocuments(state.selectedFiles); - emit( - state.copyWith(status: CustomerFilesStatus.success, selectedFiles: []), - ); - } catch (e) { - emit( - state.copyWith( - status: CustomerFilesStatus.failure, - error: e.toString(), - ), - ); - } - } - - void _toggleCustomerFileSelection( - ToggleCustomerFileSelectionEvent event, - Emitter emit, - ) { - List selectedFiles = List.from(state.selectedFiles); - if (selectedFiles.contains(event.file)) { - selectedFiles.remove(event.file); - } else { - selectedFiles.add(event.file); - } - emit(state.copyWith(selectedFiles: selectedFiles)); - } -} diff --git a/lib/features/customers/blocs/customer_files_events.dart b/lib/features/customers/blocs/customer_files_events.dart deleted file mode 100644 index b893ce8..0000000 --- a/lib/features/customers/blocs/customer_files_events.dart +++ /dev/null @@ -1,30 +0,0 @@ -part of 'customer_files_bloc.dart'; - -abstract class CustomerFilesEvent extends Equatable { - const CustomerFilesEvent(); - - @override - List get props => []; -} - -class LoadCustomerFilesEvent extends CustomerFilesEvent {} - -class UploadCustomerFileEvent extends CustomerFilesEvent { - final PlatformFile? pickedFile; - final File? photo; - const UploadCustomerFileEvent({this.pickedFile, this.photo}); -} - -class UploadMultipleCustomerFilesEvent extends CustomerFilesEvent { - final List files; - const UploadMultipleCustomerFilesEvent(this.files); - @override - List get props => [files]; -} - -class DeleteCustomerFilesEvent extends CustomerFilesEvent {} - -class ToggleCustomerFileSelectionEvent extends CustomerFilesEvent { - 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 deleted file mode 100644 index bdb525d..0000000 --- a/lib/features/customers/blocs/customer_files_state.dart +++ /dev/null @@ -1,34 +0,0 @@ -part of 'customer_files_bloc.dart'; - -enum CustomerFilesStatus { initial, loading, uploading, success, failure } - -class CustomerFilesState extends Equatable { - const CustomerFilesState({ - required this.status, - this.error, - this.customerFiles = const [], - this.selectedFiles = const [], - }); - - final CustomerFilesStatus status; - final String? error; - final List customerFiles; - final List selectedFiles; - - @override - List get props => [status, error, customerFiles, selectedFiles]; - - CustomerFilesState copyWith({ - CustomerFilesStatus? status, - String? error, - List? customerFiles, - List? selectedFiles, - }) { - return CustomerFilesState( - status: status ?? this.status, - error: error, - customerFiles: customerFiles ?? this.customerFiles, - selectedFiles: selectedFiles ?? this.selectedFiles, - ); - } -} diff --git a/lib/features/customers/ui/customer_detail_screen.dart b/lib/features/customers/ui/customer_detail_screen.dart index b6e64da..30f90aa 100644 --- a/lib/features/customers/ui/customer_detail_screen.dart +++ b/lib/features/customers/ui/customer_detail_screen.dart @@ -6,8 +6,8 @@ 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/blocs/attachments_bloc.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'; class CustomerDetailScreen extends StatefulWidget { @@ -26,11 +26,13 @@ class _CustomerDetailScreenState extends State { } void _loadFiles() { - context.read().add(LoadCustomerFilesEvent()); + context.read().add( + LoadAttachmentsEvent(parentId: widget.customer.id), + ); } Future _pickAndUpload() async { - CustomerFilesBloc customerFilesBloc = context.read(); + AttachmentsBloc attachmentsBloc = context.read(); // Chiamata statica pulita FilePickerResult? result = await FilePicker.pickFiles( @@ -40,17 +42,13 @@ class _CustomerDetailScreenState extends State { ); if (result != null) { - for (var pickedFile in result.files) { - try { - customerFilesBloc.add( - UploadCustomerFileEvent(pickedFile: pickedFile), - ); - } catch (e) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text("Errore upload ${pickedFile.name}: $e")), - ); - } + try { + attachmentsBloc.add(UploadAttachmentsEvent(pickedFiles: result.files)); + } catch (e) { + if (mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text("$e"))); } } } @@ -143,7 +141,7 @@ class _CustomerDetailScreenState extends State { } Widget _buildDocumentSection() { - return BlocBuilder( + return BlocBuilder( builder: (context, state) { return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -213,9 +211,9 @@ class _CustomerDetailScreenState extends State { ], ), const SizedBox(height: 20), - if (state.status == CustomerFilesStatus.loading) + if (state.status == AttachmentsStatus.loading) const Center(child: CircularProgressIndicator()) - else if (state.customerFiles.isEmpty) + else if (state.allFiles.isEmpty) const Center(child: Text("Nessun documento presente")) else Expanded( @@ -226,9 +224,9 @@ class _CustomerDetailScreenState extends State { crossAxisSpacing: 16, childAspectRatio: 1.2, ), - itemCount: state.customerFiles.length, + itemCount: state.allFiles.length, itemBuilder: (context, index) => - _FileCard(file: state.customerFiles[index], state: state), + _FileCard(file: state.allFiles[index], state: state), ), ), ], @@ -268,14 +266,14 @@ class _CustomerDetailScreenState extends State { class _FileCard extends StatelessWidget { final AttachmentModel file; - final CustomerFilesState state; + final AttachmentsState state; const _FileCard({required this.file, required this.state}); @override Widget build(BuildContext context) { return GestureDetector( - onTap: () => context.read().add( - ToggleCustomerFileSelectionEvent(file), + onTap: () => context.read().add( + ToggleAttachmentSelectionEvent(file), ), onDoubleTap: () => _handleDoubleClickOnFile(context, file), child: Stack( diff --git a/lib/features/customers/ui/customer_mobile_upload_screen.dart b/lib/features/customers/ui/customer_mobile_upload_screen.dart deleted file mode 100644 index 0f18079..0000000 --- a/lib/features/customers/ui/customer_mobile_upload_screen.dart +++ /dev/null @@ -1,304 +0,0 @@ -import 'dart:io'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:image_picker/image_picker.dart'; -import 'package:file_picker/file_picker.dart'; -import 'package:flux/features/customers/blocs/customer_files_bloc.dart'; - -class CustomerMobileUploadScreen extends StatefulWidget { - final String customerId; - final String customerName; - - const CustomerMobileUploadScreen({ - super.key, - required this.customerId, - required this.customerName, - }); - - @override - State createState() => - _CustomerMobileUploadScreenState(); -} - -class _CustomerMobileUploadScreenState - extends State { - // 1. LA NOSTRA STAGING AREA (Il "Carrello") - final List _stagedFiles = []; - - // 2. STATO DI CARICAMENTO GLOBALE - bool _isUploading = false; - - // Funzione magica per capire se è un'immagine o un PDF dall'estensione - bool _isImage(String path) { - final ext = path.split('.').last.toLowerCase(); - return ['jpg', 'jpeg', 'png', 'webp'].contains(ext); - } - - @override - Widget build(BuildContext context) { - return BlocListener( - listener: (context, state) { - // Quando il BLoC ci dice che ha finito l'upload (Success), chiudiamo la pagina! - if (state.status == CustomerFilesStatus.success && _isUploading) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("Tutti i file caricati con successo! ✅"), - ), - ); - Navigator.of(context).pop(); - } - if (state.status == CustomerFilesStatus.failure) { - setState(() => _isUploading = false); - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text("Errore: ${state.error}"))); - } - }, - child: Scaffold( - appBar: AppBar( - title: Text("Upload: ${widget.customerName}"), - // Togliamo la freccia indietro se stiamo caricando per evitare disastri - automaticallyImplyLeading: !_isUploading, - ), - body: Stack( - children: [ - Column( - children: [ - // --- SEZIONE PULSANTI (Fotocamera / Galleria) --- - Padding( - padding: const EdgeInsets.all(16.0), - child: Row( - children: [ - Expanded( - child: ElevatedButton.icon( - onPressed: _isUploading ? null : _handleCamera, - icon: const Icon(Icons.camera_alt), - label: const Text("SCATTA"), - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 16), - ), - ), - ), - const SizedBox(width: 12), - Expanded( - child: OutlinedButton.icon( - onPressed: _isUploading ? null : _handleFilePicker, - icon: const Icon(Icons.folder), - label: const Text("GALLERIA"), - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 16), - ), - ), - ), - ], - ), - ), - - const Divider(), - - // --- SEZIONE ANTEPRIME (La GridView Magica) --- - Expanded( - child: _stagedFiles.isEmpty - ? const Center( - child: Text( - "Nessun file selezionato.\nScatta una foto o scegli dalla galleria.", - textAlign: TextAlign.center, - style: TextStyle(color: Colors.grey), - ), - ) - : GridView.builder( - padding: const EdgeInsets.all(16), - gridDelegate: - const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: - 3, // 3 colonne come la galleria dell'iPhone - crossAxisSpacing: 12, - mainAxisSpacing: 12, - ), - itemCount: _stagedFiles.length, - itemBuilder: (context, index) { - final file = _stagedFiles[index]; - final isImg = _isImage(file.name); - - return Stack( - clipBehavior: Clip.none, - children: [ - // L'ANTEPRIMA - Container( - width: double.infinity, - height: double.infinity, - decoration: BoxDecoration( - color: Colors.grey.shade200, - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: Colors.grey.shade300, - ), - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(12), - child: isImg - ? Image.file( - File(file.path!), - fit: BoxFit.cover, - ) - : const Column( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Icon( - Icons.picture_as_pdf, - color: Colors.red, - size: 36, - ), - SizedBox(height: 4), - Text( - "PDF", - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ), - ), - - // IL PULSANTE CESTINO (In alto a destra) - Positioned( - top: -8, - right: -8, - child: GestureDetector( - onTap: () { - setState(() { - _stagedFiles.removeAt(index); - }); - }, - child: Container( - padding: const EdgeInsets.all(4), - decoration: const BoxDecoration( - color: Colors.red, - shape: BoxShape.circle, - ), - child: const Icon( - Icons.close, - color: Colors.white, - size: 16, - ), - ), - ), - ), - ], - ); - }, - ), - ), - - // --- SEZIONE INVIA E CHIUDI --- - SafeArea( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: SizedBox( - width: double.infinity, - height: 56, - child: ElevatedButton.icon( - // Il pulsante si accende SOLO se ci sono file nel carrello - onPressed: _stagedFiles.isEmpty || _isUploading - ? null - : _submitAllFiles, - icon: const Icon(Icons.cloud_upload), - label: Text( - "INVIA ${_stagedFiles.length} FILE E CHIUDI", - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - style: ElevatedButton.styleFrom( - backgroundColor: Theme.of( - context, - ).colorScheme.primary, - foregroundColor: Theme.of( - context, - ).colorScheme.onPrimary, - ), - ), - ), - ), - ), - ], - ), - - // --- OVERLAY DI CARICAMENTO (Impedisce tap multipli) --- - if (_isUploading) - Container( - color: Colors.black.withValues(alpha: 0.5), - child: const Center( - child: Card( - child: Padding( - padding: EdgeInsets.all(24.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - CircularProgressIndicator(), - SizedBox(height: 16), - Text( - "Caricamento in corso...", - style: TextStyle(fontWeight: FontWeight.bold), - ), - ], - ), - ), - ), - ), - ), - ], - ), - ), - ); - } - - // --- LOGICA FOTOCAMERA E LIBRERIA --- - Future _handleCamera() async { - final picker = ImagePicker(); - final photo = await picker.pickImage( - source: ImageSource.camera, - imageQuality: 80, - ); - if (photo != null) { - final photoBytes = await photo.readAsBytes(); // Sicuro anche per Web! - final photoSize = await photo.length(); - - final platformFile = PlatformFile( - name: photo.name, - size: photoSize, - path: photo.path, - bytes: photoBytes, // I bytes ci salvano la vita su Supabase! - ); - setState(() { - _stagedFiles.add(platformFile); // Unifichiamo tutto in un dart:io File - }); - } - } - - Future _handleFilePicker() async { - // allowMultiple: true permette di pescare 5 foto dalla galleria in un colpo solo! - final result = await FilePicker.pickFiles(allowMultiple: true); - if (result != null) { - setState(() { - _stagedFiles.addAll(result.files); - }); - } - } - - // --- LOGICA DI INVIO AL BLoC --- - void _submitAllFiles() { - setState(() => _isUploading = true); - - // 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(UploadMultipleCustomerFilesEvent(_stagedFiles)); - - // N.B: Il Navigator.pop() viene chiamato dal BlocListener in alto quando lo stato diventa "success"! - } -} diff --git a/lib/features/operations/ui/widgets/details_section.dart b/lib/features/operations/ui/widgets/details_section.dart index 9361d37..c2ab20c 100644 --- a/lib/features/operations/ui/widgets/details_section.dart +++ b/lib/features/operations/ui/widgets/details_section.dart @@ -1,7 +1,6 @@ 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/master_data/products/ui/quick_product_dialog.dart'; +import 'package:flux/core/widgets/shared_forms/shared_model_section.dart'; import 'package:flux/features/master_data/providers/blocs/provider_cubit.dart'; import 'package:flux/features/operations/blocs/operations_cubit.dart'; import 'package:flux/features/operations/models/operation_model.dart'; @@ -140,129 +139,6 @@ class DetailsSection extends StatelessWidget { ); } - void _showModelModal(BuildContext context) { - showModalBottomSheet( - context: context, - isScrollControlled: true, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(16)), - ), - builder: (modalContext) { - return DraggableScrollableSheet( - initialChildSize: 0.6, - minChildSize: 0.4, - maxChildSize: 0.9, - expand: false, - builder: (_, scrollController) { - return Column( - children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Seleziona Modello', - style: Theme.of(context).textTheme.titleLarge, - ), - IconButton( - icon: const Icon(Icons.close), - onPressed: () => Navigator.pop(modalContext), - ), - ], - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: TextField( - decoration: InputDecoration( - hintText: 'Cerca modello (es. iPhone 15...)', - prefixIcon: const Icon(Icons.search), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - onChanged: (query) => - context.read().searchModels(query), - ), - ), - Padding( - padding: const EdgeInsets.all(16.0), - child: ElevatedButton.icon( - style: ElevatedButton.styleFrom( - minimumSize: const Size.fromHeight(48), - ), - icon: const Icon(Icons.add), - label: const Text('Aggiungi Modello al Volo'), - onPressed: () async { - final operationsCubit = context.read(); - final existingBrands = context - .read() - .state - .brands; - - final newModel = await showDialog( - context: context, - builder: (dialogContext) { - return BlocProvider.value( - value: context.read(), - child: QuickProductDialog( - existingBrands: existingBrands, - ), - ); - }, - ); - - if (newModel != null) { - operationsCubit.updateOperationFields( - modelId: newModel.id, - modelDisplayName: newModel.nameWithBrand, - ); - if (context.mounted) Navigator.pop(modalContext); - } - }, - ), - ), - const Divider(), - Expanded( - child: BlocBuilder( - builder: (context, state) { - return ListView.builder( - controller: scrollController, - itemCount: state.models.length, - itemBuilder: (context, index) { - final deviceModel = state.models[index]; - return ListTile( - leading: const Icon(Icons.devices), - title: Text( - deviceModel.nameWithBrand, - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ), - onTap: () { - context - .read() - .updateOperationFields( - modelId: deviceModel.id, - modelDisplayName: deviceModel.nameWithBrand, - ); - Navigator.pop(modalContext); - }, - ); - }, - ); - }, - ), - ), - ], - ); - }, - ); - }, - ); - } - @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -334,30 +210,16 @@ class DetailsSection extends StatelessWidget { // 2. SCENARIO FIN (Ricerca Modello/Prodotto) if (currentType == 'Fin') ...[ - ListTile( - title: const Text('Seleziona Dispositivo/Prodotto'), - subtitle: Text( - (currentOp?.modelDisplayName != null && - currentOp!.modelDisplayName!.isNotEmpty) - ? currentOp!.modelDisplayName! - : 'Nessun modello selezionato', - style: TextStyle( - color: - (currentOp?.modelId == null || currentOp!.modelId!.isEmpty) - ? Colors.grey - : null, - fontWeight: - (currentOp?.modelId == null || currentOp!.modelId!.isEmpty) - ? FontWeight.normal - : FontWeight.bold, - ), - ), - trailing: const Icon(Icons.arrow_drop_down), - shape: RoundedRectangleBorder( - side: BorderSide(color: theme.dividerColor), - borderRadius: BorderRadius.circular(8), - ), - onTap: () => _showModelModal(context), + SharedModelSection( + label: 'Seleziona Dispositivo/Prodotto', + modelId: currentOp?.modelId, + modelName: currentOp?.modelDisplayName, + onModelSelected: (id, name) { + context.read().updateOperationFields( + modelId: id, + modelDisplayName: name, + ); + }, ), const SizedBox(height: 16), ],