diff --git a/ios/Podfile.lock b/ios/Podfile.lock new file mode 100644 index 0000000..95f4a16 --- /dev/null +++ b/ios/Podfile.lock @@ -0,0 +1,88 @@ +PODS: + - app_links (7.0.0): + - Flutter + - DKImagePickerController/Core (4.3.9): + - DKImagePickerController/ImageDataManager + - DKImagePickerController/Resource + - DKImagePickerController/ImageDataManager (4.3.9) + - DKImagePickerController/PhotoGallery (4.3.9): + - DKImagePickerController/Core + - DKPhotoGallery + - DKImagePickerController/Resource (4.3.9) + - DKPhotoGallery (0.0.19): + - DKPhotoGallery/Core (= 0.0.19) + - DKPhotoGallery/Model (= 0.0.19) + - DKPhotoGallery/Preview (= 0.0.19) + - DKPhotoGallery/Resource (= 0.0.19) + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Core (0.0.19): + - DKPhotoGallery/Model + - DKPhotoGallery/Preview + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Model (0.0.19): + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Preview (0.0.19): + - DKPhotoGallery/Model + - DKPhotoGallery/Resource + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Resource (0.0.19): + - SDWebImage + - SwiftyGif + - file_picker (0.0.1): + - DKImagePickerController/PhotoGallery + - Flutter + - Flutter (1.0.0) + - SDWebImage (5.21.7): + - SDWebImage/Core (= 5.21.7) + - SDWebImage/Core (5.21.7) + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + - SwiftyGif (5.4.5) + - url_launcher_ios (0.0.1): + - Flutter + +DEPENDENCIES: + - app_links (from `.symlinks/plugins/app_links/ios`) + - file_picker (from `.symlinks/plugins/file_picker/ios`) + - Flutter (from `Flutter`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + +SPEC REPOS: + trunk: + - DKImagePickerController + - DKPhotoGallery + - SDWebImage + - SwiftyGif + +EXTERNAL SOURCES: + app_links: + :path: ".symlinks/plugins/app_links/ios" + file_picker: + :path: ".symlinks/plugins/file_picker/ios" + Flutter: + :path: Flutter + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" + +SPEC CHECKSUMS: + app_links: a754cbec3c255bd4bbb4d236ecc06f28cd9a7ce8 + DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c + DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 + file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf + shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb + SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 + url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b + +PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e + +COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 1e5c694..da85de0 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -10,6 +10,8 @@ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 44490B82E7859424F77CB04B /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 975E96D2C8BBF1CF6A3F5F40 /* Pods_Runner.framework */; }; + 449A3D64DB8C9C60EBDF7DD1 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A2C92D305DE434FC3C442B0 /* Pods_RunnerTests.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; @@ -43,27 +45,44 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 1A2C92D305DE434FC3C442B0 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 35D61C73467480800D34D7BC /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 881F7F471B1559BA585653D1 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 8B6E013555080C92974ED449 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 8D4A0EA2456F02E466FCB0E1 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 975E96D2C8BBF1CF6A3F5F40 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + AB44F93458B7D70EE383A3A9 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + BDDDA09E437D9C0E7B65B3B1 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 0170592D7DFD7A1AE8221644 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 449A3D64DB8C9C60EBDF7DD1 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 44490B82E7859424F77CB04B /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -78,6 +97,15 @@ path = RunnerTests; sourceTree = ""; }; + 6A991A28CCED9666CA172E00 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 975E96D2C8BBF1CF6A3F5F40 /* Pods_Runner.framework */, + 1A2C92D305DE434FC3C442B0 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -96,6 +124,8 @@ 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, + F5D002C3092D87755D552D32 /* Pods */, + 6A991A28CCED9666CA172E00 /* Frameworks */, ); sourceTree = ""; }; @@ -124,6 +154,20 @@ path = Runner; sourceTree = ""; }; + F5D002C3092D87755D552D32 /* Pods */ = { + isa = PBXGroup; + children = ( + 881F7F471B1559BA585653D1 /* Pods-Runner.debug.xcconfig */, + BDDDA09E437D9C0E7B65B3B1 /* Pods-Runner.release.xcconfig */, + AB44F93458B7D70EE383A3A9 /* Pods-Runner.profile.xcconfig */, + 8B6E013555080C92974ED449 /* Pods-RunnerTests.debug.xcconfig */, + 35D61C73467480800D34D7BC /* Pods-RunnerTests.release.xcconfig */, + 8D4A0EA2456F02E466FCB0E1 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -131,8 +175,10 @@ isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( + 7385E42426A562D77ADB127F /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, + 0170592D7DFD7A1AE8221644 /* Frameworks */, ); buildRules = ( ); @@ -148,12 +194,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 55692154E5E0FA98E80084D6 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 6F6F1B58AD2DC9B50492B34B /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -241,6 +289,67 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; + 55692154E5E0FA98E80084D6 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 6F6F1B58AD2DC9B50492B34B /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 7385E42426A562D77ADB127F /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -382,6 +491,7 @@ }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 8B6E013555080C92974ED449 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -399,6 +509,7 @@ }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 35D61C73467480800D34D7BC /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -414,6 +525,7 @@ }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 8D4A0EA2456F02E466FCB0E1 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata index 1d526a1..21a3cc1 100644 --- a/ios/Runner.xcworkspace/contents.xcworkspacedata +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/lib/core/routes/app_router.dart b/lib/core/routes/app_router.dart index 60f3481..3cab5bd 100644 --- a/lib/core/routes/app_router.dart +++ b/lib/core/routes/app_router.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/blocs/session/session_bloc.dart'; import 'package:flux/features/auth/ui/auth_screen.dart'; import 'package:flux/features/company/ui/create_company_screen.dart'; @@ -8,7 +7,6 @@ import 'package:flux/features/customers/ui/customer_detail_screen.dart'; import 'package:flux/features/home/ui/home_screen.dart'; import 'package:flux/features/master_data/products/ui/products_screen.dart'; import 'package:flux/features/master_data/store/ui/create_store_screen.dart'; -import 'package:flux/features/services/blocs/services_cubit.dart'; import 'package:flux/features/services/models/service_model.dart'; import 'package:flux/features/services/ui/service_form_screen/service_form_screen.dart'; import 'package:go_router/go_router.dart'; diff --git a/lib/core/utils/string_extensions.dart b/lib/core/utils/string_extensions.dart index 4772f1d..3a8c756 100644 --- a/lib/core/utils/string_extensions.dart +++ b/lib/core/utils/string_extensions.dart @@ -19,4 +19,24 @@ extension MyStringExtensions on String? { }) .join(' '); } + + String fileExtension() { + if (this == null || this!.trim().isEmpty) return ''; + + final parts = this!.split('.'); + if (parts.length < 2) return ''; // Nessuna estensione trovata + + return parts.last.toLowerCase(); + } + + String fileNameWithoutExtension() { + if (this == null || this!.trim().isEmpty) return ''; + + final parts = this!.split('.'); + if (parts.length < 2) return this!; // Nessuna estensione trovata + + return parts + .sublist(0, parts.length - 1) + .join('.'); // Ritorna tutto tranne l'ultima parte + } } diff --git a/lib/features/customers/data/customer_repository.dart b/lib/features/customers/data/customer_repository.dart index 7c8491f..06302fe 100644 --- a/lib/features/customers/data/customer_repository.dart +++ b/lib/features/customers/data/customer_repository.dart @@ -1,18 +1,19 @@ -import 'dart:io'; - import 'package:file_picker/file_picker.dart'; +import 'package:flux/core/blocs/session/session_bloc.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'; import 'package:supabase_flutter/supabase_flutter.dart'; import '../models/customer_model.dart'; class CustomerRepository { - final SupabaseClient _client = GetIt.I(); + final SupabaseClient _supabase = GetIt.I(); + final String companyId = GetIt.I.get().state.company!.id; // Crea un nuovo cliente Future saveCustomer(CustomerModel customer) async { try { - final response = await _client + final response = await _supabase .from('customer') .upsert(customer.toJson()) .select() @@ -25,7 +26,7 @@ class CustomerRepository { Future updateCustomer(CustomerModel customer) async { try { - final response = await _client + final response = await _supabase .from('customer') .update(customer.toJson()) .eq('id', customer.id!) @@ -40,7 +41,7 @@ class CustomerRepository { // Recupera tutti i clienti dell'azienda Future> getCustomers(String companyId) async { try { - final response = await _client + final response = await _supabase .from('customer') .select('*, customer_file(count)') .eq('company_id', companyId) @@ -59,7 +60,7 @@ class CustomerRepository { String query, ) async { try { - final response = await _client + final response = await _supabase .from('customer') .select() .eq('company_id', companyId) @@ -75,13 +76,13 @@ class CustomerRepository { /// Recupera i file di un cliente specifico Future> getCustomerFiles(String customerId) async { try { - final response = await _client + final response = await _supabase .from('customer_file') .select() .eq('customer_id', customerId); return (response as List) - .map((f) => CustomerFileModel.fromJson(f)) + .map((f) => CustomerFileModel.fromMap(f)) .toList(); } catch (e) { throw 'Errore recupero file: $e'; @@ -90,7 +91,7 @@ class CustomerRepository { /// Salva il riferimento del file nel DB Future saveFileReference(CustomerFileModel file) async { - await _client.from('customer_file').insert(file.toJson()); + await _supabase.from('customer_file').insert(file.toMap()); } /// Carica un file e salva il riferimento nel database @@ -98,15 +99,24 @@ class CustomerRepository { required String customerId, required PlatformFile pickedFile, }) async { + final cleanFileName = pickedFile.name.replaceAll( + RegExp(r'[^a-zA-Z0-9\.\-]'), + '_', + ); + final storagePath = + '$companyId/customers/${DateTime.now().millisecondsSinceEpoch}_$cleanFileName'; + final int fileSize = pickedFile.size; + final fileToSave = CustomerFileModel( + customerId: customerId, + name: cleanFileName.fileNameWithoutExtension(), + extension: cleanFileName.fileExtension(), + url: '', + fileSize: fileSize, + ); + final String mimeType = fileToSave.extension.toLowerCase() == 'pdf' + ? 'application/pdf' + : 'image/${fileToSave.extension}'; try { - final user = _client.auth.currentUser; - if (user == null) throw 'Utente non autenticato'; - - final fileName = pickedFile.name; - final extension = pickedFile.extension ?? ''; - final path = - '${user.id}/$customerId/${DateTime.now().millisecondsSinceEpoch}_$fileName'; - // Usiamo bytes invece del path per massima compatibilità if (pickedFile.bytes == null && pickedFile.path == null) { throw 'Impossibile leggere il contenuto del file'; @@ -114,32 +124,26 @@ class CustomerRepository { // Se siamo su desktop/mobile abbiamo il path, su web abbiamo i bytes if (pickedFile.bytes != null) { - await _client.storage + await _supabase.storage .from('documents') - .uploadBinary(path, pickedFile.bytes!); - } else { - final file = File(pickedFile.path!); - await _client.storage.from('documents').upload(path, file); + .uploadBinary( + storagePath, + pickedFile.bytes!, + fileOptions: FileOptions(contentType: mimeType, upsert: true), + ); } - final String publicUrl = _client.storage + final String publicUrl = _supabase.storage .from('documents') - .getPublicUrl(path); + .getPublicUrl(storagePath); - final fileRecord = CustomerFileModel( - customerId: customerId, - name: fileName, - url: publicUrl, - extension: extension, - ); - - final response = await _client + final response = await _supabase .from('customer_file') - .insert(fileRecord.toJson()) + .insert(fileToSave.copyWith(url: publicUrl).toMap()) .select() .single(); - return CustomerFileModel.fromJson(response); + return CustomerFileModel.fromMap(response); } catch (e) { throw 'Errore durante l\'upload: $e'; } @@ -147,13 +151,16 @@ class CustomerRepository { /// Aggiorna la lista degli URL nel database Future updateCustomerDocuments(int id, List urls) async { - await _client.from('customer').update({'document_urls': urls}).eq('id', id); + await _supabase + .from('customer') + .update({'document_urls': urls}) + .eq('id', id); } /// Elimina un file dallo storage Future deleteDocument(String fullPath) async { // Il path dovrebbe essere ricavato dall'URL final path = fullPath.split('documents/').last; - await _client.storage.from('documents').remove([path]); + await _supabase.storage.from('documents').remove([path]); } } diff --git a/lib/features/customers/models/customer_file_model.dart b/lib/features/customers/models/customer_file_model.dart index c19680d..1b7b11d 100644 --- a/lib/features/customers/models/customer_file_model.dart +++ b/lib/features/customers/models/customer_file_model.dart @@ -7,6 +7,7 @@ class CustomerFileModel extends Equatable { final String url; final String extension; final DateTime? createdAt; + final int fileSize; const CustomerFileModel({ this.id, @@ -15,31 +16,76 @@ class CustomerFileModel extends Equatable { required this.url, required this.extension, this.createdAt, + required this.fileSize, }); - factory CustomerFileModel.fromJson(Map json) { + // Trasforma i byte in qualcosa di leggibile (KB, MB, GB) + String get sizeFormatted { + if (fileSize <= 0) return "0 B"; + const suffixes = ["B", "KB", "MB", "GB", "TB"]; + var i = (fileSize.toString().length - 1) ~/ 3; + if (i >= suffixes.length) i = suffixes.length - 1; + double num = fileSize / (1 << (i * 10)); + return "${num.toStringAsFixed(i == 0 ? 0 : 1)} ${suffixes[i]}"; + } + + bool get isPdf => extension.toLowerCase().replaceAll('.', '') == 'pdf'; + + CustomerFileModel copyWith({ + String? id, + String? customerId, + String? name, + String? url, + String? extension, + DateTime? createdAt, + int? fileSize, + }) { return CustomerFileModel( - id: json['id'] as String, - customerId: json['customer_id'], - name: json['name'], - url: json['url'], - extension: json['extension'] ?? '', - createdAt: json['created_at'] != null - ? DateTime.parse(json['created_at']) - : null, + id: id ?? this.id, + customerId: customerId ?? this.customerId, + name: name ?? this.name, + url: url ?? this.url, + extension: extension ?? this.extension, + createdAt: createdAt ?? this.createdAt, + fileSize: fileSize ?? this.fileSize, ); } - Map toJson() { + factory CustomerFileModel.fromMap(Map map) { + return CustomerFileModel( + id: map['id'] as String, + customerId: map['customer_id'], + name: map['name'], + url: map['url'], + extension: map['extension'] ?? '', + createdAt: map['created_at'] != null + ? DateTime.parse(map['created_at']) + : null, + fileSize: map['file_size'] is int + ? map['file_size'] + : int.tryParse(map['file_size']?.toString() ?? '0') ?? 0, + ); + } + + Map toMap() { return { if (id != null) 'id': id, 'customer_id': customerId, 'name': name, 'url': url, 'extension': extension, + 'file_size': fileSize, }; } @override - List get props => [id, customerId, name, url, extension, createdAt]; + List get props => [ + id, + customerId, + name, + url, + extension, + createdAt, + fileSize, + ]; } diff --git a/lib/features/services/blocs/services_cubit.dart b/lib/features/services/blocs/services_cubit.dart index 8c39014..0a6a241 100644 --- a/lib/features/services/blocs/services_cubit.dart +++ b/lib/features/services/blocs/services_cubit.dart @@ -1,4 +1,5 @@ import 'package:equatable/equatable.dart'; +import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/blocs/session/session_bloc.dart'; @@ -129,6 +130,7 @@ class ServicesCubit extends Cubit { companyId: _sessionBloc.state.company!.id, ), status: ServicesStatus.ready, + localAttachments: [], ), ); } @@ -208,7 +210,7 @@ class ServicesCubit extends Cubit { final serviceToSave = state.currentService!.copyWith(isBozza: isBozza); // 2. Salvataggio corazzato - await _repository.saveFullService(serviceToSave); + await _repository.saveFullService(serviceToSave, state.localAttachments); // 3. Reset e ricaricamento emit(state.copyWith(status: ServicesStatus.saved, currentService: null)); @@ -222,4 +224,18 @@ class ServicesCubit extends Cubit { ); } } + + // --- GESTIONE ALLEGATI LOCALI --- + + void addAttachments(List files) { + // Aggiungiamo i nuovi file a quelli già presenti in memoria + final updatedList = [...state.localAttachments, ...files]; + emit(state.copyWith(localAttachments: updatedList)); + } + + void removeLocalAttachment(int index) { + final updatedList = List.from(state.localAttachments); + updatedList.removeAt(index); + emit(state.copyWith(localAttachments: updatedList)); + } } diff --git a/lib/features/services/blocs/services_state.dart b/lib/features/services/blocs/services_state.dart index 00439fd..4881ecd 100644 --- a/lib/features/services/blocs/services_state.dart +++ b/lib/features/services/blocs/services_state.dart @@ -10,6 +10,7 @@ class ServicesState extends Equatable { final String query; final DateTimeRange? dateRange; final bool hasReachedMax; + final List localAttachments; const ServicesState({ required this.status, @@ -19,6 +20,7 @@ class ServicesState extends Equatable { this.query = '', this.dateRange, this.hasReachedMax = false, + this.localAttachments = const [], }); ServicesState copyWith({ @@ -29,6 +31,7 @@ class ServicesState extends Equatable { String? query, DateTimeRange? dateRange, bool? hasReachedMax, + List? localAttachments, }) { return ServicesState( status: status ?? this.status, @@ -38,6 +41,7 @@ class ServicesState extends Equatable { query: query ?? this.query, dateRange: dateRange ?? this.dateRange, hasReachedMax: hasReachedMax ?? this.hasReachedMax, + localAttachments: localAttachments ?? this.localAttachments, ); } @@ -50,5 +54,6 @@ class ServicesState extends Equatable { query, dateRange, hasReachedMax, + localAttachments, ]; } diff --git a/lib/features/services/data/services_repository.dart b/lib/features/services/data/services_repository.dart index 6922f1b..899b8da 100644 --- a/lib/features/services/data/services_repository.dart +++ b/lib/features/services/data/services_repository.dart @@ -1,10 +1,15 @@ +import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; +import 'package:flux/core/blocs/session/session_bloc.dart'; +import 'package:flux/core/utils/string_extensions.dart'; +import 'package:flux/features/services/models/service_file_model.dart'; +import 'package:get_it/get_it.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import '../models/service_model.dart'; class ServicesRepository { final _supabase = Supabase.instance.client; + final companyId = GetIt.I.get().state.company!.id; // --- RECUPERO SINGOLO SERVIZIO CON JOIN COMPLETO --- Future fetchServiceById(String id) async { @@ -16,7 +21,8 @@ class ServicesRepository { customer(nome), energy_service(*), fin_service(*), - entertainment_service(*) + entertainment_service(*), + service_file(*) ''') .eq('id', id) .single(); @@ -44,7 +50,8 @@ class ServicesRepository { customer(nome), energy_service(*), fin_service(*), - entertainment_service(*) + entertainment_service(*), + service_file(*) ''') .eq('company_id', companyId); @@ -75,7 +82,10 @@ class ServicesRepository { } // --- SALVATAGGIO COMPLETO (PRIMA PADRE, POI FIGLI) --- - Future saveFullService(ServiceModel service) async { + Future saveFullService( + ServiceModel service, + List localFiles, + ) async { try { // 1. Upsert del record principale final serviceData = await _supabase @@ -142,6 +152,63 @@ class ServicesRepository { if (insertTasks.isNotEmpty) { await Future.wait(insertTasks); } + if (localFiles.isNotEmpty) { + final List uploadTasks = []; + + for (var file in localFiles) { + // Puliamo il nome del file per evitare problemi con spazi o caratteri strani + final cleanFileName = file.name.replaceAll( + RegExp(r'[^a-zA-Z0-9\.\-]'), + '_', + ); + final storagePath = + '$companyId/services/$newId/${DateTime.now().millisecondsSinceEpoch}_$cleanFileName'; + + final int fileSize = file.size; + + final fileToSave = ServiceFileModel( + name: cleanFileName.fileNameWithoutExtension(), + extension: cleanFileName.fileExtension(), + url: '', + serviceId: newId, + fileSize: fileSize, + ); + + // Creiamo una funzione asincrona per caricare file e scrivere nel DB + Future uploadAndLink() async { + // Determiniamo il MIME type corretto in base all'estensione + final String mimeType = fileToSave.extension.toLowerCase() == 'pdf' + ? 'application/pdf' + : 'image/${fileToSave.extension}'; + // A. Upload nel Bucket Storage (usiamo i bytes così funziona anche su Web!) + await _supabase.storage + .from('documents') + .uploadBinary( + storagePath, + file.bytes!, + fileOptions: FileOptions( + contentType: + mimeType, // Diciamo a Supabase esattamente cos'è! + upsert: + true, // Opzionale: sovrascrive se esiste già un file con lo stesso nome + ), + ); + + // B. Otteniamo l'URL pubblico e scriviamo il record del file nel DB + final String publicUrl = _supabase.storage + .from('documents') + .getPublicUrl(storagePath); + await _supabase + .from('service_file') + .insert(fileToSave.copyWith(url: publicUrl).toMap()); + } + + uploadTasks.add(uploadAndLink()); + } + + // Eseguiamo tutti gli upload in parallelo per la massima velocità + await Future.wait(uploadTasks); + } } catch (e) { // Qui potresti aggiungere una logica di "rollback manuale" se necessario throw Exception('Errore durante il salvataggio corazzato: $e'); @@ -188,28 +255,4 @@ class ServicesRepository { ]; // Fallback se non c'è ancora storia } } - - Future uploadAttachment({ - required String serviceId, - required String fileName, - required Uint8List fileBytes, - }) async { - try { - // 1. Upload fisico nel bucket 'service_documents' - final path = '$serviceId/$fileName'; - await _supabase.storage - .from('service_documents') - .uploadBinary(path, fileBytes); - - // 2. Registriamo l'esistenza del file nel database - await _supabase.from('service_attachment').insert({ - 'service_id': serviceId, - 'file_path': path, - 'file_name': fileName, - 'created_at': DateTime.now().toIso8601String(), - }); - } catch (e) { - throw "Errore upload: $e"; - } - } } diff --git a/lib/features/services/models/service_file_model.dart b/lib/features/services/models/service_file_model.dart new file mode 100644 index 0000000..610e37e --- /dev/null +++ b/lib/features/services/models/service_file_model.dart @@ -0,0 +1,91 @@ +import 'package:equatable/equatable.dart'; + +class ServiceFileModel extends Equatable { + final String? id; + final DateTime? createdAt; + final String name; + final String extension; + final String url; + final String serviceId; + final int fileSize; // <--- Aggiunto + + const ServiceFileModel({ + this.id, + this.createdAt, + required this.name, + required this.extension, + required this.url, + required this.serviceId, + required this.fileSize, + }); + + // Trasforma i byte in qualcosa di leggibile (KB, MB, GB) + String get sizeFormatted { + if (fileSize <= 0) return "0 B"; + const suffixes = ["B", "KB", "MB", "GB", "TB"]; + var i = (fileSize.toString().length - 1) ~/ 3; + if (i >= suffixes.length) i = suffixes.length - 1; + double num = fileSize / (1 << (i * 10)); + return "${num.toStringAsFixed(i == 0 ? 0 : 1)} ${suffixes[i]}"; + } + + bool get isPdf => extension.toLowerCase().replaceAll('.', '') == 'pdf'; + + ServiceFileModel copyWith({ + String? id, + DateTime? createdAt, + String? name, + String? extension, + String? url, + String? serviceId, + int? fileSize, + }) { + return ServiceFileModel( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + name: name ?? this.name, + extension: extension ?? this.extension, + url: url ?? this.url, + serviceId: serviceId ?? this.serviceId, + fileSize: fileSize ?? this.fileSize, + ); + } + + factory ServiceFileModel.fromMap(Map map) { + return ServiceFileModel( + id: map['id'] as String, + createdAt: map['created_at'] != null + ? DateTime.parse(map['created_at']) + : null, + name: map['name'] ?? '', + extension: map['extension'] ?? '', + url: map['url'] ?? '', + serviceId: map['service_id']?.toString() ?? '', + fileSize: map['file_size'] is int + ? map['file_size'] + : int.tryParse(map['file_size']?.toString() ?? '0') ?? 0, + ); + } + + Map toMap() { + return { + if (id != null) 'id': id, + 'name': name, + 'extension': extension, + 'url': url, + 'service_id': serviceId, + 'file_size': fileSize, + }; + } + + @override + List get props => [ + id, + createdAt, + name, + extension, + url, + serviceId, + fileSize, + ]; +} diff --git a/lib/features/services/ui/service_form_screen/attachment_section.dart b/lib/features/services/ui/service_form_screen/attachment_section.dart new file mode 100644 index 0000000..4a31d4e --- /dev/null +++ b/lib/features/services/ui/service_form_screen/attachment_section.dart @@ -0,0 +1,120 @@ +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flux/features/services/blocs/services_cubit.dart'; + +class AttachmentsSection extends StatelessWidget { + const AttachmentsSection({super.key}); + + Future _pickFiles(BuildContext context) async { + // Usiamo withData: true fondamentale per avere i bytes e caricare su Supabase Storage + FilePickerResult? result = await FilePicker.pickFiles( + allowMultiple: true, + type: FileType.custom, + allowedExtensions: ['pdf', 'jpg', 'jpeg', 'png'], + withData: true, + ); + + if (result != null && context.mounted) { + context.read().addAttachments(result.files); + } + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final localFiles = state.localAttachments; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "DOCUMENTI ALLEGATI", + style: TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + letterSpacing: 1.2, + ), + ), + OutlinedButton.icon( + icon: const Icon(Icons.attach_file), + label: const Text("Aggiungi File"), + onPressed: () => _pickFiles(context), + ), + ], + ), + const SizedBox(height: 12), + + if (localFiles.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), + ), + ) + else + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: localFiles.length, + itemBuilder: (context, index) { + final file = localFiles[index]; + // Calcoliamo la dimensione in MB + final sizeMb = (file.size / (1024 * 1024)).toStringAsFixed(2); + + // Scegliamo un'icona in base al tipo di file + final isPdf = file.extension?.toLowerCase() == 'pdf'; + + return Card( + margin: const EdgeInsets.only(bottom: 8), + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: BorderSide(color: Colors.grey.shade300), + ), + child: ListTile( + leading: Icon( + isPdf ? Icons.picture_as_pdf : Icons.image, + color: isPdf ? Colors.red : Colors.blue, + size: 32, + ), + title: Text( + file.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + subtitle: Text("$sizeMb MB"), + trailing: IconButton( + icon: const Icon( + Icons.delete_outline, + color: Colors.red, + ), + onPressed: () => context + .read() + .removeLocalAttachment(index), + ), + ), + ); + }, + ), + ], + ); + }, + ); + } +} diff --git a/lib/features/services/ui/service_form_screen/service_form_screen.dart b/lib/features/services/ui/service_form_screen/service_form_screen.dart index a19f016..3068c00 100644 --- a/lib/features/services/ui/service_form_screen/service_form_screen.dart +++ b/lib/features/services/ui/service_form_screen/service_form_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/features/services/blocs/services_cubit.dart'; import 'package:flux/features/services/models/service_model.dart'; +import 'package:flux/features/services/ui/service_form_screen/attachment_section.dart'; import 'package:flux/features/services/ui/service_form_screen/customer_section.dart'; import 'package:flux/features/services/ui/service_form_screen/general_info_section.dart'; import 'package:flux/features/services/ui/service_form_screen/services_grid.dart'; @@ -113,7 +114,8 @@ class _ServiceFormScreenState extends State { ServicesGrid(service: service), const SizedBox(height: 32), - // TODO: _AttachmentsSection(), + AttachmentsSection(), + const SizedBox(height: 32), _buildBottomActionButtons(context, isSaving: isSaving), const SizedBox(height: 32), ],