diff --git a/lib/core/routes/app_router.dart b/lib/core/routes/app_router.dart index 624a36c..256edb4 100644 --- a/lib/core/routes/app_router.dart +++ b/lib/core/routes/app_router.dart @@ -6,6 +6,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/data/core_repository.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/models/customer_model.dart'; import 'package:flux/features/customers/ui/customer_detail_screen.dart'; import 'package:flux/features/home/ui/home_screen.dart'; @@ -90,9 +91,13 @@ class AppRouter { builder: (context, state) { // Recuperiamo l'oggetto customer passato tramite extra final customer = state.extra as CustomerModel; - return CustomerDetailScreen(customer: customer); + return BlocProvider( + create: (context) => CustomerFilesBloc(customer.id!), + child: CustomerDetailScreen(customer: customer), + ); }, ), + GoRoute( path: '/products', name: 'products', diff --git a/lib/core/utils/functions.dart b/lib/core/utils/functions.dart new file mode 100644 index 0000000..8cd368b --- /dev/null +++ b/lib/core/utils/functions.dart @@ -0,0 +1,9 @@ +// Funzione che chiede le chiavi a Supabase +import 'package:get_it/get_it.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; + +Future getSignedUrl(String storagePath) async { + return await GetIt.I().storage + .from('documents') + .createSignedUrl(storagePath, 60); // Link che si autodistrugge in 60s +} diff --git a/lib/core/widgets/image_viewer_widget.dart b/lib/core/widgets/image_viewer_widget.dart index 887c2a5..cfb90d0 100644 --- a/lib/core/widgets/image_viewer_widget.dart +++ b/lib/core/widgets/image_viewer_widget.dart @@ -1,6 +1,6 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; // <--- AGGIUNGI QUESTO +import 'package:flux/core/utils/functions.dart'; class ImageViewerWidget extends StatelessWidget { final String? storagePath; // ATTENZIONE: Ora contiene lo storagePath! @@ -12,13 +12,6 @@ class ImageViewerWidget extends StatelessWidget { 'Errore: Devi fornire un Path valido o i bytes del file!', ); - // Funzione che chiede le chiavi a Supabase - Future _getSignedUrl() async { - return await Supabase.instance.client.storage - .from('documents') - .createSignedUrl(storagePath!, 60); // Link che si autodistrugge in 60s - } - @override Widget build(BuildContext context) { return Scaffold( @@ -37,7 +30,7 @@ class ImageViewerWidget extends StatelessWidget { child: bytes != null ? Image.memory(bytes!) : FutureBuilder( - future: _getSignedUrl(), + future: getSignedUrl(storagePath!), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const CircularProgressIndicator(); diff --git a/lib/core/widgets/pdf_viewer_widget.dart b/lib/core/widgets/pdf_viewer_widget.dart index 216f4ba..8d6494d 100644 --- a/lib/core/widgets/pdf_viewer_widget.dart +++ b/lib/core/widgets/pdf_viewer_widget.dart @@ -1,5 +1,6 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; +import 'package:flux/core/utils/functions.dart'; import 'package:get_it/get_it.dart'; import 'package:pdfx/pdfx.dart'; import 'package:internet_file/internet_file.dart'; @@ -39,11 +40,7 @@ class _PdfViewerWidgetState extends State { pdfData = widget.bytes!; } else if (widget.storagePath != null && widget.storagePath!.isNotEmpty) { // SCENARIO 2: Pratica salvata, scarichiamo da Supabase (Remoto) - final signedUrl = await GetIt.I - .get() - .storage - .from('documents') - .createSignedUrl(widget.storagePath!, 60); + final signedUrl = await getSignedUrl(widget.storagePath!); pdfData = await InternetFile.get(signedUrl); } else { throw Exception("Nessun documento trovato"); diff --git a/lib/features/customers/blocs/customer_cubit.dart b/lib/features/customers/blocs/customer_cubit.dart index 64f4882..d2d3f7c 100644 --- a/lib/features/customers/blocs/customer_cubit.dart +++ b/lib/features/customers/blocs/customer_cubit.dart @@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/features/customers/data/customer_repository.dart'; +import 'package:flux/features/customers/models/customer_file_model.dart'; import 'package:flux/features/customers/models/customer_model.dart'; import 'package:get_it/get_it.dart'; diff --git a/lib/features/customers/blocs/customer_files_bloc.dart b/lib/features/customers/blocs/customer_files_bloc.dart new file mode 100644 index 0000000..ba172ea --- /dev/null +++ b/lib/features/customers/blocs/customer_files_bloc.dart @@ -0,0 +1,93 @@ +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/customers/data/customer_repository.dart'; +import 'package:flux/features/customers/models/customer_file_model.dart'; +import 'package:get_it/get_it.dart'; + +part 'customer_files_events.dart'; +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(_deleteCustomerFile); + 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(), + ), + ); + } + } + } + + Future _deleteCustomerFile( + DeleteCustomerFileEvent event, + Emitter emit, + ) async { + emit(state.copyWith(status: CustomerFilesStatus.loading)); + try { + await _repository.deleteDocument(event.file); + emit(state.copyWith(status: CustomerFilesStatus.success)); + } 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 new file mode 100644 index 0000000..7d87c49 --- /dev/null +++ b/lib/features/customers/blocs/customer_files_events.dart @@ -0,0 +1,26 @@ +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 DeleteCustomerFileEvent extends CustomerFilesEvent { + final CustomerFileModel file; + const DeleteCustomerFileEvent(this.file); +} + +class ToggleCustomerFileSelectionEvent extends CustomerFilesEvent { + final CustomerFileModel 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 new file mode 100644 index 0000000..88fbe5b --- /dev/null +++ b/lib/features/customers/blocs/customer_files_state.dart @@ -0,0 +1,34 @@ +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/blocs/customer_state.dart b/lib/features/customers/blocs/customer_state.dart index c8789bd..9e26c69 100644 --- a/lib/features/customers/blocs/customer_state.dart +++ b/lib/features/customers/blocs/customer_state.dart @@ -1,18 +1,27 @@ part of 'customer_cubit.dart'; -enum CustomerStatus { initial, loading, success, failure } +enum CustomerStatus { + initial, + loading, + filesLoading, + filesUploading, + success, + failure, +} class CustomerState extends Equatable { final CustomerStatus status; final List customers; final CustomerModel? lastCreatedCustomer; final String? errorMessage; + final List customerFiles; const CustomerState({ this.status = CustomerStatus.initial, this.customers = const [], this.lastCreatedCustomer, this.errorMessage, + this.customerFiles = const [], }); CustomerState copyWith({ @@ -20,12 +29,14 @@ class CustomerState extends Equatable { List? customers, CustomerModel? lastCreatedCustomer, String? errorMessage, + List? customerFiles, }) { return CustomerState( status: status ?? this.status, customers: customers ?? this.customers, lastCreatedCustomer: lastCreatedCustomer ?? this.lastCreatedCustomer, errorMessage: errorMessage ?? this.errorMessage, + customerFiles: customerFiles ?? this.customerFiles, ); } @@ -35,5 +46,6 @@ class CustomerState extends Equatable { customers, lastCreatedCustomer, errorMessage, + customerFiles, ]; } diff --git a/lib/features/customers/data/customer_repository.dart b/lib/features/customers/data/customer_repository.dart index 6b3f3c2..342d1a7 100644 --- a/lib/features/customers/data/customer_repository.dart +++ b/lib/features/customers/data/customer_repository.dart @@ -1,5 +1,8 @@ +import 'dart:io'; + import 'package:file_picker/file_picker.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; +import 'package:flux/core/utils/functions.dart'; import 'package:flux/core/utils/string_extensions.dart'; import 'package:flux/features/customers/models/customer_file_model.dart'; import 'package:get_it/get_it.dart'; @@ -76,6 +79,19 @@ class CustomerRepository { } } + /// Ascolta in tempo reale i file caricati per un cliente + Stream> getCustomerFilesStream(String customerId) { + return _supabase + .from('customer_file') + .stream(primaryKey: ['id']) + .eq('customer_id', customerId) + .order('created_at', ascending: false) + .map( + (listOfMaps) => + listOfMaps.map((map) => CustomerFileModel.fromMap(map)).toList(), + ); + } + /// Recupera i file di un cliente specifico Future> getCustomerFiles(String customerId) async { try { @@ -113,7 +129,7 @@ class CustomerRepository { customerId: customerId, name: cleanFileName.fileNameWithoutExtension(), extension: cleanFileName.fileExtension(), - url: storagePath, + storagePath: storagePath, fileSize: fileSize, ); final String mimeType = fileToSave.extension.toLowerCase() == 'pdf' @@ -161,9 +177,13 @@ class CustomerRepository { } /// Elimina un file dallo storage - Future deleteDocument(String fullPath) async { - // Il path dovrebbe essere ricavato dall'URL - final path = fullPath.split('documents/').last; - await _supabase.storage.from('documents').remove([path]); + Future deleteDocument(CustomerFileModel file) async { + try { + final path = await getSignedUrl(file.storagePath); + await _supabase.from('customer_file').delete().eq('id', file.id!); + await _supabase.storage.from('documents').remove([path]); + } on Exception catch (e) { + throw 'Errore durante l\'eliminazione del file: $e'; + } } } diff --git a/lib/features/customers/models/customer_file_model.dart b/lib/features/customers/models/customer_file_model.dart index 1b7b11d..00d1e92 100644 --- a/lib/features/customers/models/customer_file_model.dart +++ b/lib/features/customers/models/customer_file_model.dart @@ -4,7 +4,7 @@ class CustomerFileModel extends Equatable { final String? id; final String customerId; // Riferimento UUID final String name; - final String url; + final String storagePath; final String extension; final DateTime? createdAt; final int fileSize; @@ -13,7 +13,7 @@ class CustomerFileModel extends Equatable { this.id, required this.customerId, required this.name, - required this.url, + required this.storagePath, required this.extension, this.createdAt, required this.fileSize, @@ -35,7 +35,7 @@ class CustomerFileModel extends Equatable { String? id, String? customerId, String? name, - String? url, + String? storagePath, String? extension, DateTime? createdAt, int? fileSize, @@ -44,7 +44,7 @@ class CustomerFileModel extends Equatable { id: id ?? this.id, customerId: customerId ?? this.customerId, name: name ?? this.name, - url: url ?? this.url, + storagePath: storagePath ?? this.storagePath, extension: extension ?? this.extension, createdAt: createdAt ?? this.createdAt, fileSize: fileSize ?? this.fileSize, @@ -56,7 +56,7 @@ class CustomerFileModel extends Equatable { id: map['id'] as String, customerId: map['customer_id'], name: map['name'], - url: map['url'], + storagePath: map['storage_path'], extension: map['extension'] ?? '', createdAt: map['created_at'] != null ? DateTime.parse(map['created_at']) @@ -72,7 +72,7 @@ class CustomerFileModel extends Equatable { if (id != null) 'id': id, 'customer_id': customerId, 'name': name, - 'url': url, + 'storage_path': storagePath, 'extension': extension, 'file_size': fileSize, }; @@ -83,7 +83,7 @@ class CustomerFileModel extends Equatable { id, customerId, name, - url, + storagePath, extension, createdAt, fileSize, diff --git a/lib/features/customers/ui/customer_detail_screen.dart b/lib/features/customers/ui/customer_detail_screen.dart index 36f67b1..bdea03f 100644 --- a/lib/features/customers/ui/customer_detail_screen.dart +++ b/lib/features/customers/ui/customer_detail_screen.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; import 'package:file_picker/file_picker.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/theme/theme.dart'; -import 'package:flux/features/customers/data/customer_repository.dart'; +import 'package:flux/core/widgets/image_viewer_widget.dart'; +import 'package:flux/core/widgets/pdf_viewer_widget.dart'; +import 'package:flux/features/customers/blocs/customer_files_bloc.dart'; import 'package:flux/features/customers/models/customer_model.dart'; import 'package:flux/features/customers/models/customer_file_model.dart'; -import 'package:get_it/get_it.dart'; class CustomerDetailScreen extends StatefulWidget { final CustomerModel customer; @@ -15,36 +17,19 @@ class CustomerDetailScreen extends StatefulWidget { } class _CustomerDetailScreenState extends State { - final _repository = GetIt.I(); - List _files = []; - bool _isLoadingFiles = true; - @override void initState() { super.initState(); _loadFiles(); } - Future _loadFiles() async { - try { - final files = await _repository.getCustomerFiles( - widget.customer.id.toString(), - ); - setState(() { - _files = files; - _isLoadingFiles = false; - }); - } catch (e) { - setState(() => _isLoadingFiles = false); - if (mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(e.toString()))); - } - } + void _loadFiles() { + context.read().add(LoadCustomerFilesEvent()); } Future _pickAndUpload() async { + CustomerFilesBloc customerFilesBloc = context.read(); + // Chiamata statica pulita FilePickerResult? result = await FilePicker.pickFiles( allowMultiple: true, @@ -55,11 +40,9 @@ class _CustomerDetailScreenState extends State { if (result != null) { for (var pickedFile in result.files) { try { - final newFile = await _repository.uploadAndRegisterFile( - customerId: widget.customer.id.toString(), - pickedFile: pickedFile, + customerFilesBloc.add( + UploadCustomerFileEvent(pickedFile: pickedFile), ); - setState(() => _files.add(newFile)); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( @@ -158,46 +141,51 @@ class _CustomerDetailScreenState extends State { } Widget _buildDocumentSection() { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + return BlocBuilder( + builder: (context, state) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - "DOCUMENTI", - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: context.accent, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "DOCUMENTI", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: context.accent, + ), + ), + ElevatedButton.icon( + onPressed: _pickAndUpload, + icon: const Icon(Icons.add_circle_outline), + label: const Text("CARICA FILE"), + ), + ], + ), + const SizedBox(height: 20), + if (state.status == CustomerFilesStatus.loading) + const Center(child: CircularProgressIndicator()) + else if (state.customerFiles.isEmpty) + const Center(child: Text("Nessun documento presente")) + else + Expanded( + child: GridView.builder( + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 200, + mainAxisSpacing: 16, + crossAxisSpacing: 16, + childAspectRatio: 1.2, + ), + itemCount: state.customerFiles.length, + itemBuilder: (context, index) => + _FileCard(file: state.customerFiles[index], state: state), + ), ), - ), - ElevatedButton.icon( - onPressed: _pickAndUpload, - icon: const Icon(Icons.add_circle_outline), - label: const Text("CARICA FILE"), - ), ], - ), - const SizedBox(height: 20), - if (_isLoadingFiles) - const Center(child: CircularProgressIndicator()) - else if (_files.isEmpty) - const Center(child: Text("Nessun documento presente")) - else - Expanded( - child: GridView.builder( - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 200, - mainAxisSpacing: 16, - crossAxisSpacing: 16, - childAspectRatio: 1.2, - ), - itemCount: _files.length, - itemBuilder: (context, index) => _FileCard(file: _files[index]), - ), - ), - ], + ); + }, ); } @@ -227,30 +215,54 @@ class _CustomerDetailScreenState extends State { class _FileCard extends StatelessWidget { final CustomerFileModel file; - const _FileCard({required this.file}); + final CustomerFilesState state; + const _FileCard({required this.file, required this.state}); @override Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - color: context.background, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: context.accent.withValues(alpha: 0.1)), + return GestureDetector( + onTap: () => context.read().add( + ToggleCustomerFileSelectionEvent(file), ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, + onDoubleTap: () => _handleDoubleClickOnFile(context, file), + child: Stack( children: [ - Icon(_getFileIcon(file.extension), size: 48, color: context.accent), - const SizedBox(height: 8), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Text( - file.name, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500), + Container( + decoration: BoxDecoration( + color: context.background, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: context.accent.withValues(alpha: 0.1)), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + _getFileIcon(file.extension), + size: 48, + color: context.accent, + ), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Text( + file.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + ), + ], ), ), + if (state.selectedFiles.contains(file)) + Positioned( + top: 10, + left: 10, + child: Icon(Icons.check_circle, color: context.accent, size: 24), + ), ], ), ); @@ -268,4 +280,25 @@ class _FileCard extends StatelessWidget { return Icons.insert_drive_file; } } + + void _handleDoubleClickOnFile(BuildContext context, CustomerFileModel file) { + showDialog( + context: context, + barrierDismissible: true, + builder: (ctx) => Dialog( + insetPadding: const EdgeInsets.all(16), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: SizedBox( + width: double.infinity, + height: MediaQuery.of(context).size.height * 0.8, + child: file.isPdf + ? PdfViewerWidget(storagePath: file.storagePath) + : ImageViewerWidget(storagePath: file.storagePath), + ), + ), + ), + ); + } } diff --git a/lib/features/customers/ui/customers_content.dart b/lib/features/customers/ui/customers_content.dart index d9eb0b1..05a056b 100644 --- a/lib/features/customers/ui/customers_content.dart +++ b/lib/features/customers/ui/customers_content.dart @@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/theme/theme.dart'; import 'package:flux/features/customers/blocs/customer_cubit.dart'; +import 'package:flux/features/customers/blocs/customer_files_bloc.dart'; import 'package:flux/features/customers/models/customer_model.dart'; import 'package:flux/features/customers/ui/customer_form.dart'; import 'package:go_router/go_router.dart'; @@ -37,39 +38,6 @@ class _CustomersContentState extends State { } } - /// Funzione unica per gestire Creazione e Modifica - void _openCustomerForm({CustomerModel? customer}) { - showDialog( - context: context, - builder: (dialogContext) => AlertDialog( - backgroundColor: context.background, - content: SizedBox( - width: 500, // Larghezza ottimale per desktop - child: CustomerForm( - customer: customer, - onSave: (customerFromForm) { - final session = context.read().state; - final companyId = session.company?.id; - - if (companyId == null) return; - - if (customer == null) { - // CASO NUOVO: Iniettiamo il companyId e inviamo l'evento create - context.read().createCustomer( - customerFromForm.copyWith(companyId: companyId), - ); - } else { - // CASO MODIFICA: L'ID e il companyId sono già nel modello - context.read().updateCustomer(customerFromForm); - } - Navigator.pop(dialogContext); - }, - ), - ), - ), - ); - } - @override Widget build(BuildContext context) { return Scaffold( @@ -85,7 +53,7 @@ class _CustomersContentState extends State { Padding( padding: const EdgeInsets.only(right: 16), child: ElevatedButton.icon( - onPressed: () => _openCustomerForm(), + onPressed: () => openCustomerForm(context: context), icon: const Icon(Icons.person_add_alt_1_rounded, size: 20), label: const Text('NUOVO'), style: ElevatedButton.styleFrom( @@ -244,8 +212,48 @@ class _CustomerTile extends StatelessWidget { ], ), ), - trailing: Icon(Icons.edit_note_rounded, color: context.accent), + trailing: IconButton( + onPressed: () => + openCustomerForm(context: context, customer: customer), + icon: Icon(Icons.edit_note_rounded, color: context.accent), + ), ), ); } } + +/// Funzione unica per gestire Creazione e Modifica +void openCustomerForm({ + CustomerModel? customer, + required BuildContext context, +}) { + showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + backgroundColor: context.background, + content: SizedBox( + width: 500, // Larghezza ottimale per desktop + child: CustomerForm( + customer: customer, + onSave: (customerFromForm) { + final session = context.read().state; + final companyId = session.company?.id; + + if (companyId == null) return; + + if (customer == null) { + // CASO NUOVO: Iniettiamo il companyId e inviamo l'evento create + context.read().createCustomer( + customerFromForm.copyWith(companyId: companyId), + ); + } else { + // CASO MODIFICA: L'ID e il companyId sono già nel modello + context.read().updateCustomer(customerFromForm); + } + Navigator.pop(dialogContext); + }, + ), + ), + ), + ); +} diff --git a/lib/features/services/blocs/services_cubit.dart b/lib/features/services/blocs/services_cubit.dart index 3d62ca4..010f5aa 100644 --- a/lib/features/services/blocs/services_cubit.dart +++ b/lib/features/services/blocs/services_cubit.dart @@ -235,7 +235,7 @@ class ServicesCubit extends Cubit { serviceId: state.currentService?.id ?? '', name: file.name.fileNameWithoutExtension(), extension: file.name.fileExtension(), - url: '', + storagePath: '', fileSize: file.size, localBytes: file.bytes, createdAt: DateTime.now(), @@ -295,7 +295,7 @@ class ServicesCubit extends Cubit { orElse: () => file, ); - if (savedFile.url.isEmpty) { + if (savedFile.storagePath.isEmpty) { throw Exception( "Errore: URL del file non trovato dopo il salvataggio.", ); diff --git a/lib/features/services/data/services_repository.dart b/lib/features/services/data/services_repository.dart index a20715d..a1f6c68 100644 --- a/lib/features/services/data/services_repository.dart +++ b/lib/features/services/data/services_repository.dart @@ -159,7 +159,10 @@ class ServicesRepository { final String mimeType = file.extension.toLowerCase() == 'pdf' ? 'application/pdf' : 'image/${file.extension}'; - final fileToSave = file.copyWith(serviceId: newId, url: storagePath); + final fileToSave = file.copyWith( + serviceId: newId, + storagePath: storagePath, + ); // Creiamo una funzione asincrona per caricare file e scrivere nel DB Future uploadAndLink() async { @@ -235,6 +238,15 @@ class ServicesRepository { } } + /// Ascolta in tempo reale i file caricati per una pratica + Stream>> getServiceFilesStream(String serviceId) { + return _supabase + .from('service_file') + .stream(primaryKey: ['id']) + .eq('service_id', serviceId) + .order('created_at', ascending: false); + } + Future copyFileToCustomer({ required ServiceFileModel file, required String customerId, @@ -242,7 +254,7 @@ class ServicesRepository { CustomerFileModel fileToCopy = CustomerFileModel( customerId: customerId, name: file.name, - url: file.url, + storagePath: file.storagePath, extension: file.extension, fileSize: file.fileSize, ); diff --git a/lib/features/services/models/service_file_model.dart b/lib/features/services/models/service_file_model.dart index 689577b..3549ba3 100644 --- a/lib/features/services/models/service_file_model.dart +++ b/lib/features/services/models/service_file_model.dart @@ -7,7 +7,7 @@ class ServiceFileModel extends Equatable { final DateTime? createdAt; final String name; final String extension; - final String url; + final String storagePath; final String serviceId; final int fileSize; final Uint8List? localBytes; @@ -17,7 +17,7 @@ class ServiceFileModel extends Equatable { this.createdAt, required this.name, required this.extension, - required this.url, + required this.storagePath, required this.serviceId, required this.fileSize, this.localBytes, @@ -40,7 +40,7 @@ class ServiceFileModel extends Equatable { DateTime? createdAt, String? name, String? extension, - String? url, + String? storagePath, String? serviceId, int? fileSize, Uint8List? localBytes, @@ -50,7 +50,7 @@ class ServiceFileModel extends Equatable { createdAt: createdAt ?? this.createdAt, name: name ?? this.name, extension: extension ?? this.extension, - url: url ?? this.url, + storagePath: storagePath ?? this.storagePath, serviceId: serviceId ?? this.serviceId, fileSize: fileSize ?? this.fileSize, localBytes: localBytes ?? this.localBytes, @@ -65,7 +65,7 @@ class ServiceFileModel extends Equatable { : null, name: map['name'] ?? '', extension: map['extension'] ?? '', - url: map['url'] ?? '', + storagePath: map['storage_path'] ?? '', serviceId: map['service_id']?.toString() ?? '', fileSize: map['file_size'] is int ? map['file_size'] @@ -78,7 +78,7 @@ class ServiceFileModel extends Equatable { if (id != null) 'id': id, 'name': name, 'extension': extension, - 'url': url, + 'storage_path': storagePath, 'service_id': serviceId, 'file_size': fileSize, }; @@ -90,7 +90,7 @@ class ServiceFileModel extends Equatable { createdAt, name, extension, - url, + storagePath, serviceId, fileSize, localBytes, 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 f1e161f..70f0bef 100644 --- a/lib/features/services/ui/service_form_screen/attachment_section.dart +++ b/lib/features/services/ui/service_form_screen/attachment_section.dart @@ -169,11 +169,15 @@ class AttachmentsSection extends StatelessWidget { height: MediaQuery.of(context).size.height * 0.8, child: file.isPdf ? PdfViewerWidget( - storagePath: file.url.isNotEmpty ? file.url : null, + storagePath: file.storagePath.isNotEmpty + ? file.storagePath + : null, bytes: file.localBytes, ) : ImageViewerWidget( - storagePath: file.url.isNotEmpty ? file.url : null, + storagePath: file.storagePath.isNotEmpty + ? file.storagePath + : null, bytes: file.localBytes, ), ), diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 3792af4..e12c657 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,10 +6,14 @@ #include "generated_plugin_registrant.h" +#include #include #include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); + file_selector_plugin_register_with_registrar(file_selector_linux_registrar); g_autoptr(FlPluginRegistrar) gtk_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); gtk_plugin_register_with_registrar(gtk_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 21d8f8b..2127acd 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + file_selector_linux gtk url_launcher_linux ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 2ba73bd..425ee44 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,6 +7,7 @@ import Foundation import app_links import file_picker +import file_selector_macos import pdfx import shared_preferences_foundation import url_launcher_macos @@ -14,6 +15,7 @@ import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) PdfxPlugin.register(with: registry.registrar(forPlugin: "PdfxPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index a199ace..ea697c6 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -3,6 +3,8 @@ PODS: - FlutterMacOS - file_picker (0.0.1): - FlutterMacOS + - file_selector_macos (0.0.1): + - FlutterMacOS - FlutterMacOS (1.0.0) - pdfx (1.0.0): - FlutterMacOS @@ -15,6 +17,7 @@ PODS: DEPENDENCIES: - app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`) - file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`) + - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - pdfx (from `Flutter/ephemeral/.symlinks/plugins/pdfx/macos`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) @@ -25,6 +28,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/app_links/macos file_picker: :path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos + file_selector_macos: + :path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos FlutterMacOS: :path: Flutter/ephemeral pdfx: @@ -37,6 +42,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: app_links: 05a6ec2341985eb05e9f97dc63f5837c39895c3f file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a + file_selector_macos: 9e9e068e90ebee155097d00e89ae91edb2374db7 FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 pdfx: 1e79f57f7a6ce2f4a4c30f21fa54d3dc82441b51 shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb diff --git a/pubspec.lock b/pubspec.lock index d4d0923..80ec3e1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -185,6 +185,38 @@ packages: url: "https://pub.dev" source: hosted version: "11.0.2" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0" + url: "https://pub.dev" + source: hosted + version: "0.9.4" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a" + url: "https://pub.dev" + source: hosted + version: "0.9.5" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85" + url: "https://pub.dev" + source: hosted + version: "2.7.0" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd" + url: "https://pub.dev" + source: hosted + version: "0.9.3+5" fixnum: dependency: transitive description: @@ -328,6 +360,70 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "66810af8e99b2657ee98e5c6f02064f69bb63f7a70e343937f70946c5f8c6622" + url: "https://pub.dev" + source: hosted + version: "0.8.13+16" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588 + url: "https://pub.dev" + source: hosted + version: "0.8.13+6" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" + url: "https://pub.dev" + source: hosted + version: "0.2.2" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91" + url: "https://pub.dev" + source: hosted + version: "0.2.2+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c" + url: "https://pub.dev" + source: hosted + version: "2.11.1" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae + url: "https://pub.dev" + source: hosted + version: "0.2.2" internet_file: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index d6f54f0..732b141 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,6 +18,7 @@ dependencies: get_it: ^9.2.1 go_router: ^17.2.0 google_fonts: ^8.0.2 + image_picker: ^1.2.1 internet_file: ^1.3.0 intl: ^0.20.2 pdfx: ^2.9.2 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 1b182be..b2c13bf 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include #include @@ -14,6 +15,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { AppLinksPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("AppLinksPluginCApi")); + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); PdfxPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("PdfxPlugin")); PermissionHandlerWindowsPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 840150e..668b8b9 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST app_links + file_selector_windows pdfx permission_handler_windows url_launcher_windows