feat-insert-service #5

Merged
brontomark merged 11 commits from feat-insert-service into main 2026-04-20 16:52:20 +02:00
13 changed files with 631 additions and 80 deletions
Showing only changes of commit 78012fdbf3 - Show all commits

88
ios/Podfile.lock Normal file
View File

@@ -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

View File

@@ -10,6 +10,8 @@
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 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 */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; }; 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
@@ -43,27 +45,44 @@
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
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 = "<group>"; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; }; 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
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; }; 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 = "<group>"; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
0170592D7DFD7A1AE8221644 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
449A3D64DB8C9C60EBDF7DD1 /* Pods_RunnerTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EB1CF9000F007C117D /* Frameworks */ = { 97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
44490B82E7859424F77CB04B /* Pods_Runner.framework in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -78,6 +97,15 @@
path = RunnerTests; path = RunnerTests;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
6A991A28CCED9666CA172E00 /* Frameworks */ = {
isa = PBXGroup;
children = (
975E96D2C8BBF1CF6A3F5F40 /* Pods_Runner.framework */,
1A2C92D305DE434FC3C442B0 /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = { 9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -96,6 +124,8 @@
97C146F01CF9000F007C117D /* Runner */, 97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */, 97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */, 331C8082294A63A400263BE5 /* RunnerTests */,
F5D002C3092D87755D552D32 /* Pods */,
6A991A28CCED9666CA172E00 /* Frameworks */,
); );
sourceTree = "<group>"; sourceTree = "<group>";
}; };
@@ -124,6 +154,20 @@
path = Runner; path = Runner;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
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 = "<group>";
};
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXNativeTarget section */ /* Begin PBXNativeTarget section */
@@ -131,8 +175,10 @@
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = ( buildPhases = (
7385E42426A562D77ADB127F /* [CP] Check Pods Manifest.lock */,
331C807D294A63A400263BE5 /* Sources */, 331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */, 331C807F294A63A400263BE5 /* Resources */,
0170592D7DFD7A1AE8221644 /* Frameworks */,
); );
buildRules = ( buildRules = (
); );
@@ -148,12 +194,14 @@
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = ( buildPhases = (
55692154E5E0FA98E80084D6 /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */, 9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */, 97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */, 97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */, 97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */, 9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
6F6F1B58AD2DC9B50492B34B /* [CP] Embed Pods Frameworks */,
); );
buildRules = ( buildRules = (
); );
@@ -241,6 +289,67 @@
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; 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 */ = { 9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1; alwaysOutOfDate = 1;
@@ -382,6 +491,7 @@
}; };
331C8088294A63A400263BE5 /* Debug */ = { 331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 8B6E013555080C92974ED449 /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
@@ -399,6 +509,7 @@
}; };
331C8089294A63A400263BE5 /* Release */ = { 331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 35D61C73467480800D34D7BC /* Pods-RunnerTests.release.xcconfig */;
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
@@ -414,6 +525,7 @@
}; };
331C808A294A63A400263BE5 /* Profile */ = { 331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 8D4A0EA2456F02E466FCB0E1 /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;

View File

@@ -4,4 +4,7 @@
<FileRef <FileRef
location = "group:Runner.xcodeproj"> location = "group:Runner.xcodeproj">
</FileRef> </FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace> </Workspace>

View File

@@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/blocs/session/session_bloc.dart'; import 'package:flux/core/blocs/session/session_bloc.dart';
import 'package:flux/features/auth/ui/auth_screen.dart'; import 'package:flux/features/auth/ui/auth_screen.dart';
import 'package:flux/features/company/ui/create_company_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/home/ui/home_screen.dart';
import 'package:flux/features/master_data/products/ui/products_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/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/models/service_model.dart';
import 'package:flux/features/services/ui/service_form_screen/service_form_screen.dart'; import 'package:flux/features/services/ui/service_form_screen/service_form_screen.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';

View File

@@ -19,4 +19,24 @@ extension MyStringExtensions on String? {
}) })
.join(' '); .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
}
} }

View File

@@ -1,18 +1,19 @@
import 'dart:io';
import 'package:file_picker/file_picker.dart'; 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:flux/features/customers/models/customer_file_model.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:supabase_flutter/supabase_flutter.dart';
import '../models/customer_model.dart'; import '../models/customer_model.dart';
class CustomerRepository { class CustomerRepository {
final SupabaseClient _client = GetIt.I<SupabaseClient>(); final SupabaseClient _supabase = GetIt.I<SupabaseClient>();
final String companyId = GetIt.I.get<SessionBloc>().state.company!.id;
// Crea un nuovo cliente // Crea un nuovo cliente
Future<CustomerModel> saveCustomer(CustomerModel customer) async { Future<CustomerModel> saveCustomer(CustomerModel customer) async {
try { try {
final response = await _client final response = await _supabase
.from('customer') .from('customer')
.upsert(customer.toJson()) .upsert(customer.toJson())
.select() .select()
@@ -25,7 +26,7 @@ class CustomerRepository {
Future<CustomerModel> updateCustomer(CustomerModel customer) async { Future<CustomerModel> updateCustomer(CustomerModel customer) async {
try { try {
final response = await _client final response = await _supabase
.from('customer') .from('customer')
.update(customer.toJson()) .update(customer.toJson())
.eq('id', customer.id!) .eq('id', customer.id!)
@@ -40,7 +41,7 @@ class CustomerRepository {
// Recupera tutti i clienti dell'azienda // Recupera tutti i clienti dell'azienda
Future<List<CustomerModel>> getCustomers(String companyId) async { Future<List<CustomerModel>> getCustomers(String companyId) async {
try { try {
final response = await _client final response = await _supabase
.from('customer') .from('customer')
.select('*, customer_file(count)') .select('*, customer_file(count)')
.eq('company_id', companyId) .eq('company_id', companyId)
@@ -59,7 +60,7 @@ class CustomerRepository {
String query, String query,
) async { ) async {
try { try {
final response = await _client final response = await _supabase
.from('customer') .from('customer')
.select() .select()
.eq('company_id', companyId) .eq('company_id', companyId)
@@ -75,13 +76,13 @@ class CustomerRepository {
/// Recupera i file di un cliente specifico /// Recupera i file di un cliente specifico
Future<List<CustomerFileModel>> getCustomerFiles(String customerId) async { Future<List<CustomerFileModel>> getCustomerFiles(String customerId) async {
try { try {
final response = await _client final response = await _supabase
.from('customer_file') .from('customer_file')
.select() .select()
.eq('customer_id', customerId); .eq('customer_id', customerId);
return (response as List) return (response as List)
.map((f) => CustomerFileModel.fromJson(f)) .map((f) => CustomerFileModel.fromMap(f))
.toList(); .toList();
} catch (e) { } catch (e) {
throw 'Errore recupero file: $e'; throw 'Errore recupero file: $e';
@@ -90,7 +91,7 @@ class CustomerRepository {
/// Salva il riferimento del file nel DB /// Salva il riferimento del file nel DB
Future<void> saveFileReference(CustomerFileModel file) async { Future<void> 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 /// Carica un file e salva il riferimento nel database
@@ -98,15 +99,24 @@ class CustomerRepository {
required String customerId, required String customerId,
required PlatformFile pickedFile, required PlatformFile pickedFile,
}) async { }) 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 { 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à // Usiamo bytes invece del path per massima compatibilità
if (pickedFile.bytes == null && pickedFile.path == null) { if (pickedFile.bytes == null && pickedFile.path == null) {
throw 'Impossibile leggere il contenuto del file'; 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 // Se siamo su desktop/mobile abbiamo il path, su web abbiamo i bytes
if (pickedFile.bytes != null) { if (pickedFile.bytes != null) {
await _client.storage await _supabase.storage
.from('documents') .from('documents')
.uploadBinary(path, pickedFile.bytes!); .uploadBinary(
} else { storagePath,
final file = File(pickedFile.path!); pickedFile.bytes!,
await _client.storage.from('documents').upload(path, file); fileOptions: FileOptions(contentType: mimeType, upsert: true),
);
} }
final String publicUrl = _client.storage final String publicUrl = _supabase.storage
.from('documents') .from('documents')
.getPublicUrl(path); .getPublicUrl(storagePath);
final fileRecord = CustomerFileModel( final response = await _supabase
customerId: customerId,
name: fileName,
url: publicUrl,
extension: extension,
);
final response = await _client
.from('customer_file') .from('customer_file')
.insert(fileRecord.toJson()) .insert(fileToSave.copyWith(url: publicUrl).toMap())
.select() .select()
.single(); .single();
return CustomerFileModel.fromJson(response); return CustomerFileModel.fromMap(response);
} catch (e) { } catch (e) {
throw 'Errore durante l\'upload: $e'; throw 'Errore durante l\'upload: $e';
} }
@@ -147,13 +151,16 @@ class CustomerRepository {
/// Aggiorna la lista degli URL nel database /// Aggiorna la lista degli URL nel database
Future<void> updateCustomerDocuments(int id, List<String> urls) async { Future<void> updateCustomerDocuments(int id, List<String> 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 /// Elimina un file dallo storage
Future<void> deleteDocument(String fullPath) async { Future<void> deleteDocument(String fullPath) async {
// Il path dovrebbe essere ricavato dall'URL // Il path dovrebbe essere ricavato dall'URL
final path = fullPath.split('documents/').last; final path = fullPath.split('documents/').last;
await _client.storage.from('documents').remove([path]); await _supabase.storage.from('documents').remove([path]);
} }
} }

View File

@@ -7,6 +7,7 @@ class CustomerFileModel extends Equatable {
final String url; final String url;
final String extension; final String extension;
final DateTime? createdAt; final DateTime? createdAt;
final int fileSize;
const CustomerFileModel({ const CustomerFileModel({
this.id, this.id,
@@ -15,31 +16,76 @@ class CustomerFileModel extends Equatable {
required this.url, required this.url,
required this.extension, required this.extension,
this.createdAt, this.createdAt,
required this.fileSize,
}); });
factory CustomerFileModel.fromJson(Map<String, dynamic> 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( return CustomerFileModel(
id: json['id'] as String, id: id ?? this.id,
customerId: json['customer_id'], customerId: customerId ?? this.customerId,
name: json['name'], name: name ?? this.name,
url: json['url'], url: url ?? this.url,
extension: json['extension'] ?? '', extension: extension ?? this.extension,
createdAt: json['created_at'] != null createdAt: createdAt ?? this.createdAt,
? DateTime.parse(json['created_at']) fileSize: fileSize ?? this.fileSize,
: null,
); );
} }
Map<String, dynamic> toJson() { factory CustomerFileModel.fromMap(Map<String, dynamic> 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<String, dynamic> toMap() {
return { return {
if (id != null) 'id': id, if (id != null) 'id': id,
'customer_id': customerId, 'customer_id': customerId,
'name': name, 'name': name,
'url': url, 'url': url,
'extension': extension, 'extension': extension,
'file_size': fileSize,
}; };
} }
@override @override
List<Object?> get props => [id, customerId, name, url, extension, createdAt]; List<Object?> get props => [
id,
customerId,
name,
url,
extension,
createdAt,
fileSize,
];
} }

View File

@@ -1,4 +1,5 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/blocs/session/session_bloc.dart'; import 'package:flux/core/blocs/session/session_bloc.dart';
@@ -129,6 +130,7 @@ class ServicesCubit extends Cubit<ServicesState> {
companyId: _sessionBloc.state.company!.id, companyId: _sessionBloc.state.company!.id,
), ),
status: ServicesStatus.ready, status: ServicesStatus.ready,
localAttachments: [],
), ),
); );
} }
@@ -208,7 +210,7 @@ class ServicesCubit extends Cubit<ServicesState> {
final serviceToSave = state.currentService!.copyWith(isBozza: isBozza); final serviceToSave = state.currentService!.copyWith(isBozza: isBozza);
// 2. Salvataggio corazzato // 2. Salvataggio corazzato
await _repository.saveFullService(serviceToSave); await _repository.saveFullService(serviceToSave, state.localAttachments);
// 3. Reset e ricaricamento // 3. Reset e ricaricamento
emit(state.copyWith(status: ServicesStatus.saved, currentService: null)); emit(state.copyWith(status: ServicesStatus.saved, currentService: null));
@@ -222,4 +224,18 @@ class ServicesCubit extends Cubit<ServicesState> {
); );
} }
} }
// --- GESTIONE ALLEGATI LOCALI ---
void addAttachments(List<PlatformFile> 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<PlatformFile>.from(state.localAttachments);
updatedList.removeAt(index);
emit(state.copyWith(localAttachments: updatedList));
}
} }

View File

@@ -10,6 +10,7 @@ class ServicesState extends Equatable {
final String query; final String query;
final DateTimeRange? dateRange; final DateTimeRange? dateRange;
final bool hasReachedMax; final bool hasReachedMax;
final List<PlatformFile> localAttachments;
const ServicesState({ const ServicesState({
required this.status, required this.status,
@@ -19,6 +20,7 @@ class ServicesState extends Equatable {
this.query = '', this.query = '',
this.dateRange, this.dateRange,
this.hasReachedMax = false, this.hasReachedMax = false,
this.localAttachments = const [],
}); });
ServicesState copyWith({ ServicesState copyWith({
@@ -29,6 +31,7 @@ class ServicesState extends Equatable {
String? query, String? query,
DateTimeRange? dateRange, DateTimeRange? dateRange,
bool? hasReachedMax, bool? hasReachedMax,
List<PlatformFile>? localAttachments,
}) { }) {
return ServicesState( return ServicesState(
status: status ?? this.status, status: status ?? this.status,
@@ -38,6 +41,7 @@ class ServicesState extends Equatable {
query: query ?? this.query, query: query ?? this.query,
dateRange: dateRange ?? this.dateRange, dateRange: dateRange ?? this.dateRange,
hasReachedMax: hasReachedMax ?? this.hasReachedMax, hasReachedMax: hasReachedMax ?? this.hasReachedMax,
localAttachments: localAttachments ?? this.localAttachments,
); );
} }
@@ -50,5 +54,6 @@ class ServicesState extends Equatable {
query, query,
dateRange, dateRange,
hasReachedMax, hasReachedMax,
localAttachments,
]; ];
} }

View File

@@ -1,10 +1,15 @@
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.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 'package:supabase_flutter/supabase_flutter.dart';
import '../models/service_model.dart'; import '../models/service_model.dart';
class ServicesRepository { class ServicesRepository {
final _supabase = Supabase.instance.client; final _supabase = Supabase.instance.client;
final companyId = GetIt.I.get<SessionBloc>().state.company!.id;
// --- RECUPERO SINGOLO SERVIZIO CON JOIN COMPLETO --- // --- RECUPERO SINGOLO SERVIZIO CON JOIN COMPLETO ---
Future<ServiceModel> fetchServiceById(String id) async { Future<ServiceModel> fetchServiceById(String id) async {
@@ -16,7 +21,8 @@ class ServicesRepository {
customer(nome), customer(nome),
energy_service(*), energy_service(*),
fin_service(*), fin_service(*),
entertainment_service(*) entertainment_service(*),
service_file(*)
''') ''')
.eq('id', id) .eq('id', id)
.single(); .single();
@@ -44,7 +50,8 @@ class ServicesRepository {
customer(nome), customer(nome),
energy_service(*), energy_service(*),
fin_service(*), fin_service(*),
entertainment_service(*) entertainment_service(*),
service_file(*)
''') ''')
.eq('company_id', companyId); .eq('company_id', companyId);
@@ -75,7 +82,10 @@ class ServicesRepository {
} }
// --- SALVATAGGIO COMPLETO (PRIMA PADRE, POI FIGLI) --- // --- SALVATAGGIO COMPLETO (PRIMA PADRE, POI FIGLI) ---
Future<void> saveFullService(ServiceModel service) async { Future<void> saveFullService(
ServiceModel service,
List<PlatformFile> localFiles,
) async {
try { try {
// 1. Upsert del record principale // 1. Upsert del record principale
final serviceData = await _supabase final serviceData = await _supabase
@@ -142,6 +152,63 @@ class ServicesRepository {
if (insertTasks.isNotEmpty) { if (insertTasks.isNotEmpty) {
await Future.wait(insertTasks); await Future.wait(insertTasks);
} }
if (localFiles.isNotEmpty) {
final List<Future> 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<void> 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) { } catch (e) {
// Qui potresti aggiungere una logica di "rollback manuale" se necessario // Qui potresti aggiungere una logica di "rollback manuale" se necessario
throw Exception('Errore durante il salvataggio corazzato: $e'); throw Exception('Errore durante il salvataggio corazzato: $e');
@@ -188,28 +255,4 @@ class ServicesRepository {
]; // Fallback se non c'è ancora storia ]; // Fallback se non c'è ancora storia
} }
} }
Future<void> 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";
}
}
} }

View File

@@ -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<String, dynamic> 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<String, dynamic> toMap() {
return {
if (id != null) 'id': id,
'name': name,
'extension': extension,
'url': url,
'service_id': serviceId,
'file_size': fileSize,
};
}
@override
List<Object?> get props => [
id,
createdAt,
name,
extension,
url,
serviceId,
fileSize,
];
}

View File

@@ -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<void> _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<ServicesCubit>().addAttachments(result.files);
}
}
@override
Widget build(BuildContext context) {
return BlocBuilder<ServicesCubit, ServicesState>(
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<ServicesCubit>()
.removeLocalAttachment(index),
),
),
);
},
),
],
);
},
);
}
}

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/features/services/blocs/services_cubit.dart'; import 'package:flux/features/services/blocs/services_cubit.dart';
import 'package:flux/features/services/models/service_model.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/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/general_info_section.dart';
import 'package:flux/features/services/ui/service_form_screen/services_grid.dart'; import 'package:flux/features/services/ui/service_form_screen/services_grid.dart';
@@ -113,7 +114,8 @@ class _ServiceFormScreenState extends State<ServiceFormScreen> {
ServicesGrid(service: service), ServicesGrid(service: service),
const SizedBox(height: 32), const SizedBox(height: 32),
// TODO: _AttachmentsSection(), AttachmentsSection(),
const SizedBox(height: 32),
_buildBottomActionButtons(context, isSaving: isSaving), _buildBottomActionButtons(context, isSaving: isSaving),
const SizedBox(height: 32), const SizedBox(height: 32),
], ],