diff --git a/salesbook.Maui/Core/RestClient/IntegryApi/Dto/RegisterDeviceDTO.cs b/salesbook.Maui/Core/RestClient/IntegryApi/Dto/RegisterDeviceDTO.cs new file mode 100644 index 0000000..fc7c01d --- /dev/null +++ b/salesbook.Maui/Core/RestClient/IntegryApi/Dto/RegisterDeviceDTO.cs @@ -0,0 +1,10 @@ +using System.Text.Json.Serialization; + +namespace salesbook.Maui.Core.RestClient.IntegryApi.Dto; + +public class RegisterDeviceDTO +{ + [JsonPropertyName("userDeviceToken")] + public WtbUserDeviceTokenDTO UserDeviceToken { get; set; } + +} \ No newline at end of file diff --git a/salesbook.Maui/Core/RestClient/IntegryApi/Dto/WtbUserDeviceTokenDTO.cs b/salesbook.Maui/Core/RestClient/IntegryApi/Dto/WtbUserDeviceTokenDTO.cs new file mode 100644 index 0000000..136bd4e --- /dev/null +++ b/salesbook.Maui/Core/RestClient/IntegryApi/Dto/WtbUserDeviceTokenDTO.cs @@ -0,0 +1,22 @@ +using System.Text.Json.Serialization; + +namespace salesbook.Maui.Core.RestClient.IntegryApi.Dto; + +public class WtbUserDeviceTokenDTO +{ + [JsonPropertyName("type")] + public string Type => "wtb_user_device_tokens"; + + [JsonPropertyName("deviceToken")] + public string DeviceToken { get; set; } + + [JsonPropertyName("userName")] + public string Username { get; set; } + + [JsonPropertyName("appName")] + public int AppName => 7; //salesbook + + [JsonPropertyName("platform")] + public string Platform { get; set; } + +} \ No newline at end of file diff --git a/salesbook.Maui/Core/RestClient/IntegryApi/IntegryRegisterNotificationRestClient.cs b/salesbook.Maui/Core/RestClient/IntegryApi/IntegryRegisterNotificationRestClient.cs new file mode 100644 index 0000000..39fc676 --- /dev/null +++ b/salesbook.Maui/Core/RestClient/IntegryApi/IntegryRegisterNotificationRestClient.cs @@ -0,0 +1,38 @@ +using IntegryApiClient.Core.Domain.Abstraction.Contracts.Account; +using IntegryApiClient.Core.Domain.RestClient.Contacts; +using Microsoft.Extensions.Logging; +using salesbook.Maui.Core.RestClient.IntegryApi.Dto; +using salesbook.Shared.Core.Interface.IntegryApi; + +namespace salesbook.Maui.Core.RestClient.IntegryApi; + +public class IntegryRegisterNotificationRestClient( + ILogger logger, + IUserSession userSession, + IIntegryApiRestClient integryApiRestClient +) : IIntegryRegisterNotificationRestClient +{ + public async Task Register(string fcmToken, ILogger? logger1 = null) + { + logger1 ??= logger; + + var userDeviceToken = new RegisterDeviceDTO() + { + UserDeviceToken = new WtbUserDeviceTokenDTO() + { + DeviceToken = fcmToken, + Platform = OperatingSystem.IsAndroid() ? "Android" : "iOS", + Username = userSession.User.Username + } + }; + try + { + await integryApiRestClient.AuthorizedPost($"device_tokens/insert", userDeviceToken, + logger: logger1); + } + catch (Exception ex) + { + SentrySdk.CaptureException(ex); + } + } +} \ No newline at end of file diff --git a/salesbook.Maui/Core/Services/ManageDataService.cs b/salesbook.Maui/Core/Services/ManageDataService.cs index 9b6dc26..b9196a1 100644 --- a/salesbook.Maui/Core/Services/ManageDataService.cs +++ b/salesbook.Maui/Core/Services/ManageDataService.cs @@ -1,10 +1,13 @@ using AutoMapper; +using MudBlazor.Extensions; using salesbook.Shared.Core.Dto; using salesbook.Shared.Core.Dto.Activity; using salesbook.Shared.Core.Dto.Contact; using salesbook.Shared.Core.Entity; using salesbook.Shared.Core.Helpers.Enum; using salesbook.Shared.Core.Interface; +using salesbook.Shared.Core.Interface.IntegryApi; +using salesbook.Shared.Core.Interface.System.Network; using Sentry.Protocol; using System.Linq.Expressions; @@ -209,6 +212,15 @@ public class ManageDataService( { var dto = mapper.Map(activity); + if (activity is { AlarmTime: not null, EstimatedTime: not null }) + { + var minuteBefore = activity.EstimatedTime.Value - activity.AlarmTime.Value; + dto.MinuteBefore = (int)Math.Abs(minuteBefore.TotalMinutes); + + dto.NotificationDate = dto.MinuteBefore == 0 ? + activity.EstimatedTime : activity.AlarmTime; + } + if (activity.CodJcom != null) { dto.Category = ActivityCategoryEnum.Commessa; diff --git a/salesbook.Maui/Core/Services/SyncDbService.cs b/salesbook.Maui/Core/Services/SyncDbService.cs index d5c55bb..795f99a 100644 --- a/salesbook.Maui/Core/Services/SyncDbService.cs +++ b/salesbook.Maui/Core/Services/SyncDbService.cs @@ -1,5 +1,6 @@ using salesbook.Shared.Core.Helpers; using salesbook.Shared.Core.Interface; +using salesbook.Shared.Core.Interface.IntegryApi; namespace salesbook.Maui.Core.Services; diff --git a/salesbook.Maui/Core/Services/NetworkService.cs b/salesbook.Maui/Core/System/Network/NetworkService.cs similarity index 74% rename from salesbook.Maui/Core/Services/NetworkService.cs rename to salesbook.Maui/Core/System/Network/NetworkService.cs index 5920c78..af1da33 100644 --- a/salesbook.Maui/Core/Services/NetworkService.cs +++ b/salesbook.Maui/Core/System/Network/NetworkService.cs @@ -1,6 +1,7 @@ using salesbook.Shared.Core.Interface; +using salesbook.Shared.Core.Interface.System.Network; -namespace salesbook.Maui.Core.Services; +namespace salesbook.Maui.Core.System.Network; public class NetworkService : INetworkService { diff --git a/salesbook.Maui/Core/System/Notification/FirebaseNotificationService.cs b/salesbook.Maui/Core/System/Notification/FirebaseNotificationService.cs new file mode 100644 index 0000000..8d0fd36 --- /dev/null +++ b/salesbook.Maui/Core/System/Notification/FirebaseNotificationService.cs @@ -0,0 +1,37 @@ +using salesbook.Shared.Core.Interface; +using salesbook.Shared.Core.Interface.IntegryApi; +using Shiny; +using Shiny.Notifications; +using Shiny.Push; + +namespace salesbook.Maui.Core.System.Notification; + +public class FirebaseNotificationService( + IPushManager pushManager, + IIntegryRegisterNotificationRestClient integryRegisterNotificationRestClient, + INotificationManager notificationManager +) : IFirebaseNotificationService +{ + public async Task InitFirebase() + { + CreateNotificationChannel(); + + var (accessState, token) = await pushManager.RequestAccess(); + + if (accessState == AccessState.Denied || token is null) return; + await integryRegisterNotificationRestClient.Register(token); + } + + private void CreateNotificationChannel() + { + var channel = new Channel + { + Identifier = "salesbook_push", + Description = "Notifiche push di SalesBook", + Importance = ChannelImportance.High, + Actions = [] + }; + + notificationManager.AddChannel(channel); + } +} \ No newline at end of file diff --git a/salesbook.Maui/Core/System/Notification/Push/PushNotificationDelegate.cs b/salesbook.Maui/Core/System/Notification/Push/PushNotificationDelegate.cs new file mode 100644 index 0000000..5f0bdc7 --- /dev/null +++ b/salesbook.Maui/Core/System/Notification/Push/PushNotificationDelegate.cs @@ -0,0 +1,45 @@ +using CommunityToolkit.Mvvm.Messaging; +using salesbook.Shared.Core.Entity; +using salesbook.Shared.Core.Interface.IntegryApi; +using salesbook.Shared.Core.Messages.Notification; +using Shiny.Push; + +namespace salesbook.Maui.Core.System.Notification.Push; + +public class PushNotificationDelegate( + IIntegryRegisterNotificationRestClient integryRegisterNotificationRestClient, + IMessenger messenger +) : IPushDelegate +{ + public Task OnEntry(PushNotification notification) + { + // fires when the user taps on a push notification + return Task.CompletedTask; + } + + public Task OnReceived(PushNotification notification) + { + if (notification.Notification is null) return Task.CompletedTask; + var pushNotification = new WtbNotification + { + Title = notification.Notification.Title, + Body = notification.Notification.Message + }; + + messenger.Send(new NewPushNotificationMessage(pushNotification)); + + return Task.CompletedTask; + } + + public Task OnNewToken(string token) + { + integryRegisterNotificationRestClient.Register(token); + return Task.CompletedTask; + } + + public Task OnUnRegistered(string token) + { + // fires when a push notification change is set by the operating system or provider + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/salesbook.Maui/Core/System/Notification/ShinyNotificationManager.cs b/salesbook.Maui/Core/System/Notification/ShinyNotificationManager.cs new file mode 100644 index 0000000..ab1ce6b --- /dev/null +++ b/salesbook.Maui/Core/System/Notification/ShinyNotificationManager.cs @@ -0,0 +1,9 @@ +using salesbook.Shared.Core.Interface.System.Notification; +using Shiny.Notifications; + +namespace salesbook.Maui.Core.System.Notification; + +public class ShinyNotificationManager(INotificationManager notificationManager) : IShinyNotificationManager +{ + public Task RequestAccess() => notificationManager.RequestAccess(); +} \ No newline at end of file diff --git a/salesbook.Maui/GoogleService-Info.plist b/salesbook.Maui/GoogleService-Info.plist new file mode 100644 index 0000000..4ed039f --- /dev/null +++ b/salesbook.Maui/GoogleService-Info.plist @@ -0,0 +1,30 @@ + + + + + API_KEY + AIzaSyC_QtQpsVortjzgl-B7__IQZ-85lOct55E + GCM_SENDER_ID + 830771692001 + PLIST_VERSION + 1 + BUNDLE_ID + it.integry.salesbook + PROJECT_ID + salesbook-smetar + STORAGE_BUCKET + salesbook-smetar.firebasestorage.app + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:830771692001:ios:59d8b1d8570ac81f3752a0 + + \ No newline at end of file diff --git a/salesbook.Maui/MauiProgram.cs b/salesbook.Maui/MauiProgram.cs index fac4479..4abc81f 100644 --- a/salesbook.Maui/MauiProgram.cs +++ b/salesbook.Maui/MauiProgram.cs @@ -1,4 +1,3 @@ -using AutoMapper; using CommunityToolkit.Maui; using CommunityToolkit.Mvvm.Messaging; using IntegryApiClient.MAUI; @@ -6,17 +5,26 @@ using Microsoft.AspNetCore.Components.Authorization; using Microsoft.Extensions.Logging; using MudBlazor.Services; using MudExtensions.Services; +using salesbook.Maui.Core.RestClient.IntegryApi; using salesbook.Maui.Core.Services; +using salesbook.Maui.Core.System.Network; +using salesbook.Maui.Core.System.Notification; +using salesbook.Maui.Core.System.Notification.Push; using salesbook.Shared; using salesbook.Shared.Core.Dto; using salesbook.Shared.Core.Dto.PageState; using salesbook.Shared.Core.Helpers; using salesbook.Shared.Core.Interface; +using salesbook.Shared.Core.Interface.IntegryApi; +using salesbook.Shared.Core.Interface.System.Network; +using salesbook.Shared.Core.Interface.System.Notification; using salesbook.Shared.Core.Messages.Activity.Copy; using salesbook.Shared.Core.Messages.Activity.New; using salesbook.Shared.Core.Messages.Back; using salesbook.Shared.Core.Messages.Contact; +using salesbook.Shared.Core.Messages.Notification; using salesbook.Shared.Core.Services; +using Shiny; namespace salesbook.Maui { @@ -24,7 +32,7 @@ namespace salesbook.Maui { private const string AppToken = "f0484398-1f8b-42f5-ab79-5282c164e1d8"; - public static MauiApp CreateMauiApp() + public static MauiAppBuilder CreateMauiAppBuilder() { InteractiveRenderSettings.ConfigureBlazorHybridRenderModes(); @@ -33,6 +41,7 @@ namespace salesbook.Maui .UseMauiApp() .UseIntegry(appToken: AppToken, useLoginAzienda: true) .UseMauiCommunityToolkit() + .UseShiny() .UseSentry(options => { options.Dsn = "https://453b6b38f94fd67e40e0d5306d6caff8@o4508499810254848.ingest.de.sentry.io/4509605099667536"; @@ -63,14 +72,24 @@ namespace salesbook.Maui builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); //Message - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + + //Notification + builder.Services.AddNotifications(); + builder.Services.AddPush(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); #if DEBUG builder.Services.AddBlazorWebViewDeveloperTools(); @@ -82,7 +101,7 @@ namespace salesbook.Maui builder.Services.AddSingleton(); builder.Services.AddSingleton(); - return builder.Build(); + return builder; } } } \ No newline at end of file diff --git a/salesbook.Maui/Platforms/Android/AndroidManifest.xml b/salesbook.Maui/Platforms/Android/AndroidManifest.xml index ddb278b..e38ccd8 100644 --- a/salesbook.Maui/Platforms/Android/AndroidManifest.xml +++ b/salesbook.Maui/Platforms/Android/AndroidManifest.xml @@ -1,9 +1,24 @@  - - + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/salesbook.Maui/Platforms/Android/AndroidModule.cs b/salesbook.Maui/Platforms/Android/AndroidModule.cs new file mode 100644 index 0000000..19ab9b1 --- /dev/null +++ b/salesbook.Maui/Platforms/Android/AndroidModule.cs @@ -0,0 +1,13 @@ +using salesbook.Maui.Core; +using salesbook.Shared.Core.Interface.System.Battery; + +namespace salesbook.Maui; + +public static class AndroidModule +{ + public static MauiAppBuilder RegisterAndroidAppServices(this MauiAppBuilder mauiAppBuilder) + { + mauiAppBuilder.Services.AddSingleton(); + return mauiAppBuilder; + } +} \ No newline at end of file diff --git a/salesbook.Maui/Platforms/Android/Core/BatteryOptimizationManagerService.cs b/salesbook.Maui/Platforms/Android/Core/BatteryOptimizationManagerService.cs new file mode 100644 index 0000000..4e1f3ff --- /dev/null +++ b/salesbook.Maui/Platforms/Android/Core/BatteryOptimizationManagerService.cs @@ -0,0 +1,28 @@ +using Android.App; +using Android.Content; +using Android.OS; +using Android.Provider; +using salesbook.Shared.Core.Interface.System.Battery; +using Application = Android.App.Application; + +namespace salesbook.Maui.Core; + +public class BatteryOptimizationManagerService : IBatteryOptimizationManagerService +{ + public bool IsBatteryOptimizationEnabled() + { + var packageName = AppInfo.PackageName; + + var pm = (PowerManager)Application.Context.GetSystemService(Context.PowerService)!; + return !pm.IsIgnoringBatteryOptimizations(packageName); + } + + public void OpenBatteryOptimizationSettings(Action onCompleted) + { + var packageName = AppInfo.PackageName; + + var intent = new Intent(Settings.ActionRequestIgnoreBatteryOptimizations); + intent.SetData(Android.Net.Uri.Parse("package:" + packageName)); + ((MainActivity)Platform.CurrentActivity!).StartActivityForResult(intent, (result, _) => { onCompleted(result == Result.Ok); }); + } +} \ No newline at end of file diff --git a/salesbook.Maui/Platforms/Android/MainActivity.cs b/salesbook.Maui/Platforms/Android/MainActivity.cs index 1c93fe9..dbc05ea 100644 --- a/salesbook.Maui/Platforms/Android/MainActivity.cs +++ b/salesbook.Maui/Platforms/Android/MainActivity.cs @@ -1,13 +1,42 @@ using Android.App; +using Android.Content; using Android.Content.PM; namespace salesbook.Maui { - [Activity(Theme = "@style/Maui.SplashTheme", + [Activity( + Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)] + + [IntentFilter( + [ + Shiny.ShinyPushIntents.NotificationClickAction + ], + Categories = + [ + "android.intent.category.DEFAULT" + ] + )] public class MainActivity : MauiAppCompatActivity { + private readonly IDictionary> _onActivityResultSubscriber = + new Dictionary>(); + + public void StartActivityForResult(Intent intent, Action onResultAction) + { + var requestCode = new Random(DateTime.Now.Millisecond).Next(); + _onActivityResultSubscriber.Add(requestCode, onResultAction); + StartActivityForResult(intent, requestCode); + } + + protected override void OnActivityResult(int requestCode, Result resultCode, Intent data) + { + if (_onActivityResultSubscriber.TryGetValue(requestCode, out var value)) + value(resultCode, data); + + base.OnActivityResult(requestCode, resultCode, data); + } } } \ No newline at end of file diff --git a/salesbook.Maui/Platforms/Android/MainApplication.cs b/salesbook.Maui/Platforms/Android/MainApplication.cs index 2820e44..ff3db19 100644 --- a/salesbook.Maui/Platforms/Android/MainApplication.cs +++ b/salesbook.Maui/Platforms/Android/MainApplication.cs @@ -1,16 +1,16 @@ using Android.App; using Android.Runtime; -namespace salesbook.Maui -{ - [Application(HardwareAccelerated = true)] - public class MainApplication : MauiApplication - { - public MainApplication(IntPtr handle, JniHandleOwnership ownership) - : base(handle, ownership) - { - } +namespace salesbook.Maui; - protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +[Application(HardwareAccelerated = true)] +public class MainApplication : MauiApplication +{ + public MainApplication(IntPtr handle, JniHandleOwnership ownership) + : base(handle, ownership) + { } -} + + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiAppBuilder() + .RegisterAndroidAppServices().Build(); +} \ No newline at end of file diff --git a/salesbook.Maui/Platforms/iOS/AppDelegate.cs b/salesbook.Maui/Platforms/iOS/AppDelegate.cs index 57062da..0728e77 100644 --- a/salesbook.Maui/Platforms/iOS/AppDelegate.cs +++ b/salesbook.Maui/Platforms/iOS/AppDelegate.cs @@ -1,10 +1,24 @@ using Foundation; +using UIKit; namespace salesbook.Maui { [Register("AppDelegate")] public class AppDelegate : MauiUIApplicationDelegate { - protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiAppBuilder() + .RegisterIosAppServices().Build(); + + [Export("application:didRegisterForRemoteNotificationsWithDeviceToken:")] + public void RegisteredForRemoteNotifications(UIApplication application, NSData deviceToken) + => global::Shiny.Hosting.Host.Lifecycle.OnRegisteredForRemoteNotifications(deviceToken); + + [Export("application:didFailToRegisterForRemoteNotificationsWithError:")] + public void FailedToRegisterForRemoteNotifications(UIApplication application, NSError error) + => global::Shiny.Hosting.Host.Lifecycle.OnFailedToRegisterForRemoteNotifications(error); + + [Export("application:didReceiveRemoteNotification:fetchCompletionHandler:")] + public void DidReceiveRemoteNotification(UIApplication application, NSDictionary userInfo, Action completionHandler) + => global::Shiny.Hosting.Host.Lifecycle.OnDidReceiveRemoteNotification(userInfo, completionHandler); } } diff --git a/salesbook.Maui/Platforms/iOS/Core/BatteryOptimizationManagerService.cs b/salesbook.Maui/Platforms/iOS/Core/BatteryOptimizationManagerService.cs new file mode 100644 index 0000000..3f9400d --- /dev/null +++ b/salesbook.Maui/Platforms/iOS/Core/BatteryOptimizationManagerService.cs @@ -0,0 +1,12 @@ +using salesbook.Shared.Core.Interface.System.Battery; + +namespace salesbook.Maui.Core; + +public class BatteryOptimizationManagerService : IBatteryOptimizationManagerService +{ + public bool IsBatteryOptimizationEnabled() => true; + + public void OpenBatteryOptimizationSettings(Action onCompleted) + { + } +} \ No newline at end of file diff --git a/salesbook.Maui/Platforms/iOS/Info.plist b/salesbook.Maui/Platforms/iOS/Info.plist index a094b3e..0c883a2 100644 --- a/salesbook.Maui/Platforms/iOS/Info.plist +++ b/salesbook.Maui/Platforms/iOS/Info.plist @@ -45,5 +45,9 @@ NSPhotoLibraryAddUsageDescription Permette all'app di salvare file o immagini nella tua libreria fotografica se necessario. + UIBackgroundModes + + remote-notification + diff --git a/salesbook.Maui/Platforms/iOS/PrivacyInfo.xcprivacy b/salesbook.Maui/Platforms/iOS/PrivacyInfo.xcprivacy new file mode 100644 index 0000000..fcd26e7 --- /dev/null +++ b/salesbook.Maui/Platforms/iOS/PrivacyInfo.xcprivacy @@ -0,0 +1,110 @@ + + + + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryFileTimestamp + NSPrivacyAccessedAPITypeReasons + + C617.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategorySystemBootTime + NSPrivacyAccessedAPITypeReasons + + 35F9.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryDiskSpace + NSPrivacyAccessedAPITypeReasons + + E174.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPITypeReasons + + CA92.1 + + + + NSPrivacyCollectedDataTypes + + + + NSPrivacyCollectedDataTypeUserID + NSPrivacyCollectedDataTypeLocation + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAppFunctionality + + + + NSPrivacyCollectedDataTypeEmailAddress + NSPrivacyCollectedDataTypeLocation + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAppFunctionality + + + + NSPrivacyCollectedDataTypePhoneNumber + NSPrivacyCollectedDataTypeLocation + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAppFunctionality + + + + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeOtherDiagnosticData + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAppFunctionality + + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeCrashData + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAppFunctionality + + + + + + + diff --git a/salesbook.Maui/Platforms/iOS/iOSModule.cs b/salesbook.Maui/Platforms/iOS/iOSModule.cs new file mode 100644 index 0000000..071675e --- /dev/null +++ b/salesbook.Maui/Platforms/iOS/iOSModule.cs @@ -0,0 +1,13 @@ +using salesbook.Maui.Core; +using salesbook.Shared.Core.Interface.System.Battery; + +namespace salesbook.Maui; + +public static class iOSModule +{ + public static MauiAppBuilder RegisterIosAppServices(this MauiAppBuilder mauiAppBuilder) + { + mauiAppBuilder.Services.AddSingleton(); + return mauiAppBuilder; + } +} \ No newline at end of file diff --git a/salesbook.Maui/google-services.json b/salesbook.Maui/google-services.json new file mode 100644 index 0000000..ef98865 --- /dev/null +++ b/salesbook.Maui/google-services.json @@ -0,0 +1,29 @@ +{ + "project_info": { + "project_number": "830771692001", + "project_id": "salesbook-smetar", + "storage_bucket": "salesbook-smetar.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:830771692001:android:06bc5a9706bc9bef3752a0", + "android_client_info": { + "package_name": "it.integry.salesbook" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyB43ai_Ph0phO_OkBC1wAOazKZUV9KsLaM" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/salesbook.Maui/salesbook.Maui.csproj b/salesbook.Maui/salesbook.Maui.csproj index 4c87907..9b32639 100644 --- a/salesbook.Maui/salesbook.Maui.csproj +++ b/salesbook.Maui/salesbook.Maui.csproj @@ -83,14 +83,26 @@ manual - + - --> + + + + + + + + PreserveNewest + + + + + @@ -121,13 +133,16 @@ - + + + + diff --git a/salesbook.Maui/wwwroot/index.html b/salesbook.Maui/wwwroot/index.html index 74faadd..7f7b15f 100644 --- a/salesbook.Maui/wwwroot/index.html +++ b/salesbook.Maui/wwwroot/index.html @@ -53,6 +53,7 @@ + diff --git a/salesbook.Shared/Components/Layout/MainLayout.razor b/salesbook.Shared/Components/Layout/MainLayout.razor index dc8d62b..514030f 100644 --- a/salesbook.Shared/Components/Layout/MainLayout.razor +++ b/salesbook.Shared/Components/Layout/MainLayout.razor @@ -1,5 +1,7 @@ @using System.Globalization @using salesbook.Shared.Core.Interface +@using salesbook.Shared.Core.Interface.IntegryApi +@using salesbook.Shared.Core.Interface.System.Network @using salesbook.Shared.Core.Messages.Back @inherits LayoutComponentBase @inject IJSRuntime JS @@ -55,7 +57,7 @@ private bool _showWarning; private DateTime _lastApiCheck = DateTime.MinValue; - private const int DelaySeconds = 60; + private const int DelaySeconds = 180; private CancellationTokenSource? _cts; diff --git a/salesbook.Shared/Components/Layout/NavMenu.razor b/salesbook.Shared/Components/Layout/NavMenu.razor index a4cdf53..f5c158d 100644 --- a/salesbook.Shared/Components/Layout/NavMenu.razor +++ b/salesbook.Shared/Components/Layout/NavMenu.razor @@ -1,13 +1,17 @@ @using CommunityToolkit.Mvvm.Messaging @using salesbook.Shared.Core.Dto @using salesbook.Shared.Core.Dto.Activity +@using salesbook.Shared.Core.Dto.PageState @using salesbook.Shared.Core.Entity @using salesbook.Shared.Core.Messages.Activity.Copy @using salesbook.Shared.Core.Messages.Activity.New @using salesbook.Shared.Core.Messages.Contact +@using salesbook.Shared.Core.Messages.Notification @inject IDialogService Dialog @inject IMessenger Messenger @inject CopyActivityService CopyActivityService +@inject NewPushNotificationService NewPushNotificationService +@inject NotificationState Notification
@@ -63,8 +69,9 @@ protected override Task OnInitializedAsync() { CopyActivityService.OnCopyActivity += async dto => await CreateActivity(dto); + NewPushNotificationService.OnNotificationReceived += NewNotificationReceived; - NavigationManager.LocationChanged += (_, args) => + NavigationManager.LocationChanged += (_, args) => { var location = args.Location.Remove(0, NavigationManager.BaseUri.Length); @@ -104,4 +111,10 @@ Messenger.Send(new NewContactMessage((CRMCreateContactResponseDTO)result.Data)); } } + + private void NewNotificationReceived(WtbNotification notification) + { + Notification.ReceivedNotifications.Add(notification); + InvokeAsync(StateHasChanged); + } } \ No newline at end of file diff --git a/salesbook.Shared/Components/Pages/Calendar.razor.css b/salesbook.Shared/Components/Pages/Calendar.razor.css index 8475a60..df9aa85 100644 --- a/salesbook.Shared/Components/Pages/Calendar.razor.css +++ b/salesbook.Shared/Components/Pages/Calendar.razor.css @@ -24,6 +24,7 @@ padding-bottom: 1rem; overflow-x: hidden; overflow-y: visible; + min-height: 7rem; } .week-slider.expanded { @@ -31,7 +32,7 @@ grid-template-columns: repeat(7, 1fr); gap: 0.4rem; padding: 1rem; - overflow-y: auto; + overflow-y: hidden; } .week-slider.expand-animation { animation: expandFromCenter 0.3s ease forwards; } @@ -122,8 +123,7 @@ flex-direction: column; -ms-overflow-style: none; scrollbar-width: none; - padding-bottom: 16vh; - height: calc(100% - 130px); + padding-bottom: 9vh; } .appointments.ah-calendar-m { height: calc(100% - 315px) !important; } diff --git a/salesbook.Shared/Components/Pages/Commessa.razor b/salesbook.Shared/Components/Pages/Commessa.razor index 24d8b14..829abce 100644 --- a/salesbook.Shared/Components/Pages/Commessa.razor +++ b/salesbook.Shared/Components/Pages/Commessa.razor @@ -11,6 +11,7 @@ @using salesbook.Shared.Core.Dto.PageState @using salesbook.Shared.Core.Entity @using salesbook.Shared.Core.Interface +@using salesbook.Shared.Core.Interface.IntegryApi @inject JobSteps JobSteps @inject IManageDataService ManageData @inject IIntegryApiService IntegryApiService diff --git a/salesbook.Shared/Components/Pages/Home.razor b/salesbook.Shared/Components/Pages/Home.razor index d8e89ab..aadb903 100644 --- a/salesbook.Shared/Components/Pages/Home.razor +++ b/salesbook.Shared/Components/Pages/Home.razor @@ -2,9 +2,13 @@ @attribute [Authorize] @using salesbook.Shared.Core.Interface @using salesbook.Shared.Components.Layout.Spinner +@using salesbook.Shared.Core.Interface.System.Network +@using salesbook.Shared.Core.Interface.System.Notification @using salesbook.Shared.Core.Services @inject IFormFactor FormFactor @inject INetworkService NetworkService +@inject IFirebaseNotificationService FirebaseNotificationService +@inject IShinyNotificationManager NotificationManager @inject PreloadService PreloadService @@ -13,6 +17,17 @@ { protected override async Task OnInitializedAsync() { + await CheckAndRequestPermissions(); + + try + { + await FirebaseNotificationService.InitFirebase(); + } + catch (Exception e) + { + Console.WriteLine($"Firebase init: {e.Message}"); + } + var lastSyncDate = LocalStorage.Get("last-sync"); if (!FormFactor.IsWeb() && NetworkService.ConnectionAvailable && lastSyncDate.Equals(DateTime.MinValue)) @@ -25,6 +40,14 @@ NavigationManager.NavigateTo("/Calendar"); } + private async Task CheckAndRequestPermissions() + { + await NotificationManager.RequestAccess(); + + // if (BatteryOptimizationManagerService.IsBatteryOptimizationEnabled()) + // BatteryOptimizationManagerService.OpenBatteryOptimizationSettings(_ => { }); + } + private Task StartSyncUser() { return Task.Run(() => diff --git a/salesbook.Shared/Components/Pages/Login.razor b/salesbook.Shared/Components/Pages/Login.razor index baafd52..bbf2048 100644 --- a/salesbook.Shared/Components/Pages/Login.razor +++ b/salesbook.Shared/Components/Pages/Login.razor @@ -1,6 +1,7 @@ @page "/login" @using salesbook.Shared.Components.Layout.Spinner @using salesbook.Shared.Core.Interface +@using salesbook.Shared.Core.Interface.System.Network @using salesbook.Shared.Core.Services @inject IUserAccountService UserAccountService @inject AppAuthenticationStateProvider AuthenticationStateProvider diff --git a/salesbook.Shared/Components/Pages/Notifications.razor b/salesbook.Shared/Components/Pages/Notifications.razor index 9579556..c4cfe74 100644 --- a/salesbook.Shared/Components/Pages/Notifications.razor +++ b/salesbook.Shared/Components/Pages/Notifications.razor @@ -1,14 +1,179 @@ @page "/Notifications" @attribute [Authorize] @using salesbook.Shared.Components.Layout +@using salesbook.Shared.Components.Layout.Spinner @using salesbook.Shared.Components.SingleElements +@using salesbook.Shared.Core.Dto.PageState +@using salesbook.Shared.Core.Entity +@using salesbook.Shared.Core.Interface.IntegryApi +@using salesbook.Shared.Core.Messages.Notification +@inject NotificationState Notification +@inject NewPushNotificationService NewPushNotificationService +@inject IJSRuntime JS +@inject IIntegryNotificationRestClient IntegryNotificationRestClient
- + @if (Loading) + { + + + } + else + { + if (Notification.ReceivedNotifications.IsNullOrEmpty() && Notification.UnreadNotifications.IsNullOrEmpty() && Notification.NotificationsRead.IsNullOrEmpty()) + { + + } + else + { +
+ @foreach(var notification in Notification.ReceivedNotifications) + { + + } + + @foreach(var notification in Notification.UnreadNotifications) + { + + } + + @foreach (var notification in Notification.NotificationsRead) + { + + } +
+ } + }
@code { + private DotNetObjectReference? _objectReference; + private bool Loading { get; set; } = true; + protected override Task OnInitializedAsync() + { + _objectReference = DotNetObjectReference.Create(this); + NewPushNotificationService.OnNotificationReceived += NewNotificationReceived; + return Task.CompletedTask; + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await JS.InvokeVoidAsync("initNotifications", _objectReference); + + if (firstRender) + { + await LoadData(); + Loading = false; + StateHasChanged(); + } + } + + private async Task LoadData() + { + var allNotifications = await IntegryNotificationRestClient.Get(); + var allIds = allNotifications.Select(n => n.Id).ToHashSet(); + + Notification.ReceivedNotifications = Notification.ReceivedNotifications + .Where(r => !allIds.Contains(r.Id)) + .ToList(); + + Notification.UnreadNotifications = allNotifications + .Where(x => + x.WtbDeviceNotifications == null || + x.WtbDeviceNotifications.Any(y => y.ReadDate == null)) + .ToList(); + + Notification.NotificationsRead = allNotifications + .Where(x => + x.WtbDeviceNotifications != null && + x.WtbDeviceNotifications.All(y => y.ReadDate != null)) + .ToList(); + + OrderNotificationList(); + } + + private void NewNotificationReceived(WtbNotification notification) + { + InvokeAsync(StateHasChanged); + } + + [JSInvokable] + public async Task Delete(string id) + { + Loading = true; + _ = InvokeAsync(StateHasChanged); + + if (!long.TryParse(id, out var notificationId)) return; + + var removed = false; + + if (Notification.ReceivedNotifications.RemoveAll(x => x.Id == notificationId) > 0) + removed = true; + else if (Notification.UnreadNotifications.RemoveAll(x => x.Id == notificationId) > 0) + removed = true; + else if (Notification.NotificationsRead.RemoveAll(x => x.Id == notificationId) > 0) + removed = true; + + if (!removed) return; + + OrderNotificationList(); + Loading = false; + _ = InvokeAsync(StateHasChanged); + + _ = Task.Run(() => + { + _ = IntegryNotificationRestClient.Delete(notificationId); + }); + } + + [JSInvokable] + public async Task MarkAsRead(string id) + { + Loading = true; + _ = InvokeAsync(StateHasChanged); + + var notificationId = long.Parse(id); + + var wtbNotification = Notification.ReceivedNotifications + .FindLast(x => x.Id == notificationId); + + if (wtbNotification == null) + { + wtbNotification = Notification.UnreadNotifications + .FindLast(x => x.Id == notificationId); + + Notification.UnreadNotifications.Remove(wtbNotification!); + } + else + { + Notification.ReceivedNotifications.Remove(wtbNotification); + } + + wtbNotification = await IntegryNotificationRestClient.MarkAsRead(notificationId); + Notification.NotificationsRead.Add(wtbNotification); + + OrderNotificationList(); + Loading = false; + StateHasChanged(); + } + + private void OrderNotificationList() + { + Notification.ReceivedNotifications = Notification.ReceivedNotifications + .OrderByDescending(x => x.StartDate).ToList(); + + Notification.UnreadNotifications = Notification.UnreadNotifications + .OrderByDescending(x => x.StartDate).ToList(); + + Notification.NotificationsRead = Notification.NotificationsRead + .OrderByDescending(x => x.StartDate).ToList(); + } + + public void Dispose() + { + _objectReference?.Dispose(); + } } \ No newline at end of file diff --git a/salesbook.Shared/Components/Pages/Notifications.razor.css b/salesbook.Shared/Components/Pages/Notifications.razor.css index e69de29..6f08dcd 100644 --- a/salesbook.Shared/Components/Pages/Notifications.razor.css +++ b/salesbook.Shared/Components/Pages/Notifications.razor.css @@ -0,0 +1,6 @@ +.list { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; +} diff --git a/salesbook.Shared/Components/Pages/PersonalInfo.razor b/salesbook.Shared/Components/Pages/PersonalInfo.razor index 036fcbf..8ac584b 100644 --- a/salesbook.Shared/Components/Pages/PersonalInfo.razor +++ b/salesbook.Shared/Components/Pages/PersonalInfo.razor @@ -3,6 +3,7 @@ @using salesbook.Shared.Components.Layout @using salesbook.Shared.Core.Authorization.Enum @using salesbook.Shared.Core.Interface +@using salesbook.Shared.Core.Interface.System.Network @using salesbook.Shared.Core.Services @inject AppAuthenticationStateProvider AuthenticationStateProvider @inject INetworkService NetworkService diff --git a/salesbook.Shared/Components/Pages/User.razor b/salesbook.Shared/Components/Pages/User.razor index 0da73a8..aff2c4d 100644 --- a/salesbook.Shared/Components/Pages/User.razor +++ b/salesbook.Shared/Components/Pages/User.razor @@ -10,6 +10,7 @@ @using salesbook.Shared.Core.Dto.Activity @using salesbook.Shared.Core.Dto.JobProgress @using salesbook.Shared.Core.Dto.PageState +@using salesbook.Shared.Core.Interface.IntegryApi @implements IAsyncDisposable @inject IManageDataService ManageData @inject IMapper Mapper diff --git a/salesbook.Shared/Components/Pages/User.razor.css b/salesbook.Shared/Components/Pages/User.razor.css index b69a8da..68f62dd 100644 --- a/salesbook.Shared/Components/Pages/User.razor.css +++ b/salesbook.Shared/Components/Pages/User.razor.css @@ -143,6 +143,7 @@ max-height: 32vh; overflow: auto; scrollbar-width: none; + padding: 1rem 0; } .container-pers-rif::-webkit-scrollbar { display: none; } diff --git a/salesbook.Shared/Components/Pages/Users.razor.css b/salesbook.Shared/Components/Pages/Users.razor.css index 06a5b75..0de27f4 100644 --- a/salesbook.Shared/Components/Pages/Users.razor.css +++ b/salesbook.Shared/Components/Pages/Users.razor.css @@ -5,8 +5,7 @@ flex-direction: column; -ms-overflow-style: none; scrollbar-width: none; - padding-bottom: 70px; - height: 100%; + padding-bottom: 9vh; } .users .divider { diff --git a/salesbook.Shared/Components/SingleElements/BottomSheet/SearchAddress.razor b/salesbook.Shared/Components/SingleElements/BottomSheet/SearchAddress.razor index 44a29f7..c6f0b7b 100644 --- a/salesbook.Shared/Components/SingleElements/BottomSheet/SearchAddress.razor +++ b/salesbook.Shared/Components/SingleElements/BottomSheet/SearchAddress.razor @@ -2,6 +2,7 @@ @using salesbook.Shared.Core.Dto @using salesbook.Shared.Core.Interface @using salesbook.Shared.Components.Layout.Overlay +@using salesbook.Shared.Core.Interface.IntegryApi @inject IIntegryApiService IntegryApiService
diff --git a/salesbook.Shared/Components/SingleElements/Card/AttachCard.razor b/salesbook.Shared/Components/SingleElements/Card/AttachCard.razor index 26d27a8..3d0d085 100644 --- a/salesbook.Shared/Components/SingleElements/Card/AttachCard.razor +++ b/salesbook.Shared/Components/SingleElements/Card/AttachCard.razor @@ -1,5 +1,6 @@ @using salesbook.Shared.Core.Dto @using salesbook.Shared.Core.Interface +@using salesbook.Shared.Core.Interface.IntegryApi @inject IIntegryApiService IntegryApiService @inject IAttachedService AttachedService diff --git a/salesbook.Shared/Components/SingleElements/Card/NotificationCard.razor b/salesbook.Shared/Components/SingleElements/Card/NotificationCard.razor new file mode 100644 index 0000000..4b544c3 --- /dev/null +++ b/salesbook.Shared/Components/SingleElements/Card/NotificationCard.razor @@ -0,0 +1,86 @@ +@using salesbook.Shared.Core.Entity + +
+
+ +
+
+ +
+
+ @if (Notification.NotificationData is { Type: not null }) + { + @switch (Notification.NotificationData.Type) + { + case "memo": +
+ break; + case "newPlanned": +
+ break; + } + } +
+
+
@Notification.Title
+ +
+
@GetTimeAgo(Notification.StartDate)
+ @if (Unread) + { + + } +
+
+ + @if ( + Notification.StartDate < DateTime.Today && Notification.Body != null && Notification.Body.Contains("Oggi") + ) + { + @Notification.Body.Replace("Oggi", $"{Notification.StartDate:d}") + } + else + { + @Notification.Body + } +
+
+
+ +@code { + [Parameter] public bool Unread { get; set; } + [Parameter] public WtbNotification Notification { get; set; } = new(); + + private static string GetTimeAgo(DateTime? timestamp) + { + if (timestamp is null) return ""; + + var difference = DateTime.Now - timestamp.Value; + + switch (difference.TotalMinutes) + { + case < 1: + return "Adesso"; + case < 60: + return $"{(int)difference.TotalMinutes} minuti fa"; + default: + { + switch (difference.TotalHours) + { + case < 2: + return $"{(int)difference.TotalHours} ora fa"; + case < 24: + return $"{timestamp.Value:t}"; + default: + { + return difference.TotalDays < 7 ? $"{(int)difference.TotalDays}g fa" : timestamp.Value.ToString("dd/MM/yyyy"); + } + } + } + } + } +} \ No newline at end of file diff --git a/salesbook.Shared/Components/SingleElements/Card/NotificationCard.razor.css b/salesbook.Shared/Components/SingleElements/Card/NotificationCard.razor.css new file mode 100644 index 0000000..c199ca6 --- /dev/null +++ b/salesbook.Shared/Components/SingleElements/Card/NotificationCard.razor.css @@ -0,0 +1,105 @@ +.row { + position: relative; + overflow: hidden; + border-radius: var(--mud-default-borderradius); + box-shadow: var(--custom-box-shadow); + width: 100%; +} + +.behind-left, .behind-right { + position: absolute; + inset: 0; + display: flex; + align-items: center; + z-index: 0; +} + +.behind-right { + justify-content: flex-end; + padding-right: 14px; + background: var(--mud-palette-error); +} + +.behind-left { + justify-content: flex-start; + padding-left: 14px; + background: var(--mud-palette-info); +} + +.read-btn, .trash-btn { + color: white; + padding: 10px 15px; + cursor: pointer; +} + +.notification-card { + position: relative; + z-index: 1; + display: flex; + align-items: center; + padding: 12px; + gap: 12px; + background: var(--mud-palette-background); + transition: transform .2s ease; + touch-action: pan-y; + will-change: transform; + transform: translateX(0); +} + +.avatar { + min-width: 42px; + height: 42px; + border-radius: 12px; + display: grid; + place-items: center; + background: var(--mud-palette-background); + border: 1px solid var(--mud-palette-divider); + font-weight: bold; + color: var(--mud-palette-primary); +} + +.notification-body { + width: 100% +} + +.title-row { + display: flex; + gap: 8px; + justify-items: center; + align-items: flex-start; + justify-content: space-between; +} + +.unread-dot { + width: 10px; + height: 10px; + background-color: var(--mud-palette-error); + border-radius: 50%; +} + +.title { + font-weight: 700; + margin-bottom: unset !important; +} + +.section-time { + display: flex; + align-items: center; + gap: .5rem; +} + +.notification-time { + font-size: 13px; + color: #94a3b8; + line-height: normal; +} + +.collapsing { + transition: height .22s ease, margin .22s ease, opacity .22s ease; + overflow: hidden; +} + +.subtitle { + font-size: 12px; + color: var(--mud-palette-drawer-text); +} \ No newline at end of file diff --git a/salesbook.Shared/Components/SingleElements/Modal/ActivityForm.razor b/salesbook.Shared/Components/SingleElements/Modal/ActivityForm.razor index 352cdc7..e067500 100644 --- a/salesbook.Shared/Components/SingleElements/Modal/ActivityForm.razor +++ b/salesbook.Shared/Components/SingleElements/Modal/ActivityForm.razor @@ -9,6 +9,8 @@ @using salesbook.Shared.Core.Dto.Contact @using salesbook.Shared.Core.Entity @using salesbook.Shared.Core.Interface +@using salesbook.Shared.Core.Interface.IntegryApi +@using salesbook.Shared.Core.Interface.System.Network @using salesbook.Shared.Core.Messages.Activity.Copy @inject IManageDataService ManageData @inject INetworkService NetworkService @@ -72,9 +74,18 @@
- Avviso + Avviso - + + Nessuno + All'ora pianificata + 15 minuti prima + 30 minuti prima + 1 ora prima + 2 ore prima + 1 giorno prima + 1 settimana prima +
@@ -82,7 +93,7 @@
Assegnata a - + @foreach (var user in Users) { @user.FullName @@ -374,17 +385,22 @@ return false; } - private async Task LoadData() + private Task LoadData() { - if (!IsNew && Id != null) + return Task.Run(async () => { - ActivityFileList = await IntegryApiService.GetActivityFile(Id); - } + if (!IsNew && Id != null) + { + ActivityFileList = await IntegryApiService.GetActivityFile(Id); + } - Users = await ManageData.GetTable(); - ActivityResult = await ManageData.GetTable(); - Clienti = await ManageData.GetClienti(new WhereCondContact {FlagStato = "A"}); - Pros = await ManageData.GetProspect(); + Users = await ManageData.GetTable(); + ActivityResult = await ManageData.GetTable(); + Clienti = await ManageData.GetClienti(new WhereCondContact {FlagStato = "A"}); + Pros = await ManageData.GetProspect(); + + await InvokeAsync(StateHasChanged); + }); } private async Task LoadActivityType() @@ -446,6 +462,24 @@ OnAfterChangeValue(); } + private void OnAfterChangeTimeBefore() + { + if (ActivityModel.EstimatedTime != null) + { + if (ActivityModel.MinuteBefore != -1) + { + ActivityModel.NotificationDate = ActivityModel.MinuteBefore == 0 ? + ActivityModel.EstimatedTime : ActivityModel.EstimatedTime.Value.AddMinutes(ActivityModel.MinuteBefore * -1); + } + else + { + ActivityModel.NotificationDate = null; + } + } + + OnAfterChangeValue(); + } + private void OnAfterChangeValue() { if (!IsNew) diff --git a/salesbook.Shared/Components/SingleElements/Modal/ContactForm.razor b/salesbook.Shared/Components/SingleElements/Modal/ContactForm.razor index 8b6ffbd..46f9fa9 100644 --- a/salesbook.Shared/Components/SingleElements/Modal/ContactForm.razor +++ b/salesbook.Shared/Components/SingleElements/Modal/ContactForm.razor @@ -5,6 +5,8 @@ @using salesbook.Shared.Core.Entity @using salesbook.Shared.Components.SingleElements.BottomSheet @using salesbook.Shared.Core.Dto.Contact +@using salesbook.Shared.Core.Interface.IntegryApi +@using salesbook.Shared.Core.Interface.System.Network @inject IManageDataService ManageData @inject INetworkService NetworkService @inject IIntegryApiService IntegryApiService diff --git a/salesbook.Shared/Components/SingleElements/Modal/PersRifForm.razor b/salesbook.Shared/Components/SingleElements/Modal/PersRifForm.razor index 3e107ae..c3813b2 100644 --- a/salesbook.Shared/Components/SingleElements/Modal/PersRifForm.razor +++ b/salesbook.Shared/Components/SingleElements/Modal/PersRifForm.razor @@ -2,6 +2,8 @@ @using salesbook.Shared.Components.Layout @using salesbook.Shared.Core.Interface @using salesbook.Shared.Components.Layout.Overlay +@using salesbook.Shared.Core.Interface.IntegryApi +@using salesbook.Shared.Core.Interface.System.Network @inject IManageDataService ManageData @inject INetworkService NetworkService @inject IIntegryApiService IntegryApiService diff --git a/salesbook.Shared/Core/Dto/Activity/ActivityDTO.cs b/salesbook.Shared/Core/Dto/Activity/ActivityDTO.cs index 9888ece..e62cba0 100644 --- a/salesbook.Shared/Core/Dto/Activity/ActivityDTO.cs +++ b/salesbook.Shared/Core/Dto/Activity/ActivityDTO.cs @@ -1,4 +1,5 @@ -using salesbook.Shared.Core.Entity; +using System.Text.Json.Serialization; +using salesbook.Shared.Core.Entity; using salesbook.Shared.Core.Helpers.Enum; namespace salesbook.Shared.Core.Dto.Activity; @@ -10,6 +11,11 @@ public class ActivityDTO : StbActivity public ActivityCategoryEnum Category { get; set; } public bool Complete { get; set; } + //Notification + public int MinuteBefore { get; set; } = -1; + [JsonPropertyName("notificationDate")] + public DateTime? NotificationDate { get; set; } + public bool Deleted { get; set; } public PositionDTO? Position { get; set; } @@ -23,6 +29,9 @@ public class ActivityDTO : StbActivity { return Commessa == other.Commessa && Cliente == other.Cliente && + Position == other.Position && + MinuteBefore == other.MinuteBefore && + NotificationDate == other.NotificationDate && Category == other.Category && Complete == other.Complete && ActivityId == other.ActivityId && ActivityResultId == other.ActivityResultId && ActivityTypeId == other.ActivityTypeId && DataInsAct.Equals(other.DataInsAct) && ActivityDescription == other.ActivityDescription && ParentActivityId == other.ParentActivityId && TipoAnag == other.TipoAnag && CodAnag == other.CodAnag && CodJcom == other.CodJcom && CodJfas == other.CodJfas && Nullable.Equals(EstimatedDate, other.EstimatedDate) && Nullable.Equals(EstimatedTime, other.EstimatedTime) && Nullable.Equals(AlarmDate, other.AlarmDate) && Nullable.Equals(AlarmTime, other.AlarmTime) && Nullable.Equals(EffectiveDate, other.EffectiveDate) && Nullable.Equals(EffectiveTime, other.EffectiveTime) && ResultDescription == other.ResultDescription && Nullable.Equals(EstimatedEnddate, other.EstimatedEnddate) && Nullable.Equals(EstimatedEndtime, other.EstimatedEndtime) && Nullable.Equals(EffectiveEnddate, other.EffectiveEnddate) && Nullable.Equals(EffectiveEndtime, other.EffectiveEndtime) && UserCreator == other.UserCreator && UserName == other.UserName && Nullable.Equals(PercComp, other.PercComp) && Nullable.Equals(EstimatedHours, other.EstimatedHours) && CodMart == other.CodMart && PartitaMag == other.PartitaMag && Matricola == other.Matricola && Priorita == other.Priorita && Nullable.Equals(ActivityPlayCounter, other.ActivityPlayCounter) && ActivityEvent == other.ActivityEvent && Guarantee == other.Guarantee && Note == other.Note && Rfid == other.Rfid && IdLotto == other.IdLotto && PersonaRif == other.PersonaRif && HrNum == other.HrNum && Gestione == other.Gestione && Nullable.Equals(DataOrd, other.DataOrd) && NumOrd == other.NumOrd && IdStep == other.IdStep && IdRiga == other.IdRiga && Nullable.Equals(OraInsAct, other.OraInsAct) && IndiceGradimento == other.IndiceGradimento && NoteGradimento == other.NoteGradimento && FlagRisolto == other.FlagRisolto && FlagTipologia == other.FlagTipologia && OreRapportino == other.OreRapportino && UserModifier == other.UserModifier && Nullable.Equals(OraModAct, other.OraModAct) && Nullable.Equals(OraViewAct, other.OraViewAct) && CodVdes == other.CodVdes && CodCmac == other.CodCmac && WrikeId == other.WrikeId && CodMgrp == other.CodMgrp && PlanId == other.PlanId; } @@ -97,6 +106,9 @@ public class ActivityDTO : StbActivity hashCode.Add(Cliente); hashCode.Add(Category); hashCode.Add(Complete); + hashCode.Add(Position); + hashCode.Add(MinuteBefore); + hashCode.Add(NotificationDate); return hashCode.ToHashCode(); } } \ No newline at end of file diff --git a/salesbook.Shared/Core/Dto/NotificationDataDTO.cs b/salesbook.Shared/Core/Dto/NotificationDataDTO.cs new file mode 100644 index 0000000..8e1537d --- /dev/null +++ b/salesbook.Shared/Core/Dto/NotificationDataDTO.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace salesbook.Shared.Core.Dto; + +public class NotificationDataDTO +{ + [JsonPropertyName("activityId")] + public string? ActivityId { get; set; } + + [JsonPropertyName("type")] + public string? Type { get; set; } +} \ No newline at end of file diff --git a/salesbook.Shared/Core/Dto/PageState/NotificationState.cs b/salesbook.Shared/Core/Dto/PageState/NotificationState.cs new file mode 100644 index 0000000..79c61b4 --- /dev/null +++ b/salesbook.Shared/Core/Dto/PageState/NotificationState.cs @@ -0,0 +1,12 @@ +using salesbook.Shared.Core.Entity; + +namespace salesbook.Shared.Core.Dto.PageState; + +public class NotificationState +{ + public List ReceivedNotifications { get; set; } = []; + public List UnreadNotifications { get; set; } = []; + public List NotificationsRead { get; set; } = []; + + public int Count => ReceivedNotifications.Count() + UnreadNotifications.Count(); +} \ No newline at end of file diff --git a/salesbook.Shared/Core/Dto/ReadNotificationRequestDTO.cs b/salesbook.Shared/Core/Dto/ReadNotificationRequestDTO.cs new file mode 100644 index 0000000..d8b448e --- /dev/null +++ b/salesbook.Shared/Core/Dto/ReadNotificationRequestDTO.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace salesbook.Shared.Core.Dto; + +public class ReadNotificationRequestDTO +{ + [JsonPropertyName("deviceToken")] + public string DeviceToken { get; set; } + + [JsonPropertyName("username")] + public string Username { get; set; } + + [JsonPropertyName("notificationId")] + public long NotificationId { get; set; } +} \ No newline at end of file diff --git a/salesbook.Shared/Core/Entity/WtbDeviceNotification.cs b/salesbook.Shared/Core/Entity/WtbDeviceNotification.cs new file mode 100644 index 0000000..0d0b375 --- /dev/null +++ b/salesbook.Shared/Core/Entity/WtbDeviceNotification.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace salesbook.Shared.Core.Entity; + +public class WtbDeviceNotification +{ + [JsonPropertyName("userDeviceId")] + public long? UserDeviceId { get; set; } + + [JsonPropertyName("notificationId")] + public long? NotificationId { get; set; } + + [JsonPropertyName("readDate")] + public DateTime? ReadDate { get; set; } +} \ No newline at end of file diff --git a/salesbook.Shared/Core/Entity/WtbNotification.cs b/salesbook.Shared/Core/Entity/WtbNotification.cs new file mode 100644 index 0000000..d4ed855 --- /dev/null +++ b/salesbook.Shared/Core/Entity/WtbNotification.cs @@ -0,0 +1,37 @@ +using System.Text.Json.Serialization; +using salesbook.Shared.Core.Dto; + +namespace salesbook.Shared.Core.Entity; + +public class WtbNotification +{ + [JsonPropertyName("id")] + public long Id { get; set; } + + [JsonPropertyName("title")] + public string? Title { get; set; } + + [JsonPropertyName("body")] + public string? Body { get; set; } + + [JsonPropertyName("imageUrl")] + public string? ImageUrl { get; set; } + + [JsonPropertyName("notificationData")] + public NotificationDataDTO? NotificationData { get; set; } + + [JsonPropertyName("startDate")] + public DateTime? StartDate { get; set; } + + [JsonPropertyName("endDate")] + public DateTime? EndDate { get; set; } + + [JsonPropertyName("persistent")] + public bool? Persistent { get; set; } + + [JsonPropertyName("topics")] + public List? Topics { get; set; } + + [JsonPropertyName("wtbDeviceNotifications")] + public List? WtbDeviceNotifications { get; set; } +} \ No newline at end of file diff --git a/salesbook.Shared/Core/Interface/IFirebaseNotificationService.cs b/salesbook.Shared/Core/Interface/IFirebaseNotificationService.cs new file mode 100644 index 0000000..47c6707 --- /dev/null +++ b/salesbook.Shared/Core/Interface/IFirebaseNotificationService.cs @@ -0,0 +1,6 @@ +namespace salesbook.Shared.Core.Interface; + +public interface IFirebaseNotificationService +{ + Task InitFirebase(); +} \ No newline at end of file diff --git a/salesbook.Shared/Core/Interface/IIntegryApiService.cs b/salesbook.Shared/Core/Interface/IntegryApi/IIntegryApiService.cs similarity index 97% rename from salesbook.Shared/Core/Interface/IIntegryApiService.cs rename to salesbook.Shared/Core/Interface/IntegryApi/IIntegryApiService.cs index a8fdc7d..1cd51af 100644 --- a/salesbook.Shared/Core/Interface/IIntegryApiService.cs +++ b/salesbook.Shared/Core/Interface/IntegryApi/IIntegryApiService.cs @@ -4,7 +4,7 @@ using salesbook.Shared.Core.Dto.Contact; using salesbook.Shared.Core.Dto.JobProgress; using salesbook.Shared.Core.Entity; -namespace salesbook.Shared.Core.Interface; +namespace salesbook.Shared.Core.Interface.IntegryApi; public interface IIntegryApiService { diff --git a/salesbook.Shared/Core/Interface/IntegryApi/IIntegryNotificationRestClient.cs b/salesbook.Shared/Core/Interface/IntegryApi/IIntegryNotificationRestClient.cs new file mode 100644 index 0000000..e396beb --- /dev/null +++ b/salesbook.Shared/Core/Interface/IntegryApi/IIntegryNotificationRestClient.cs @@ -0,0 +1,11 @@ +using salesbook.Shared.Core.Entity; + +namespace salesbook.Shared.Core.Interface.IntegryApi; + +public interface IIntegryNotificationRestClient +{ + Task> Get(); + Task MarkAsRead(long id); + Task Delete(long id); + Task DeleteAll(); +} \ No newline at end of file diff --git a/salesbook.Shared/Core/Interface/IntegryApi/IIntegryRegisterNotificationRestClient.cs b/salesbook.Shared/Core/Interface/IntegryApi/IIntegryRegisterNotificationRestClient.cs new file mode 100644 index 0000000..e62e891 --- /dev/null +++ b/salesbook.Shared/Core/Interface/IntegryApi/IIntegryRegisterNotificationRestClient.cs @@ -0,0 +1,8 @@ +using Microsoft.Extensions.Logging; + +namespace salesbook.Shared.Core.Interface.IntegryApi; + +public interface IIntegryRegisterNotificationRestClient +{ + Task Register(string fcmToken, ILogger? logger1 = null); +} \ No newline at end of file diff --git a/salesbook.Shared/Core/Interface/System/Battery/IBatteryOptimizationManagerService.cs b/salesbook.Shared/Core/Interface/System/Battery/IBatteryOptimizationManagerService.cs new file mode 100644 index 0000000..179f1ac --- /dev/null +++ b/salesbook.Shared/Core/Interface/System/Battery/IBatteryOptimizationManagerService.cs @@ -0,0 +1,8 @@ +namespace salesbook.Shared.Core.Interface.System.Battery; + +public interface IBatteryOptimizationManagerService +{ + bool IsBatteryOptimizationEnabled(); + + void OpenBatteryOptimizationSettings(Action onCompleted); +} \ No newline at end of file diff --git a/salesbook.Shared/Core/Interface/INetworkService.cs b/salesbook.Shared/Core/Interface/System/Network/INetworkService.cs similarity index 67% rename from salesbook.Shared/Core/Interface/INetworkService.cs rename to salesbook.Shared/Core/Interface/System/Network/INetworkService.cs index 1bec94c..43b6acc 100644 --- a/salesbook.Shared/Core/Interface/INetworkService.cs +++ b/salesbook.Shared/Core/Interface/System/Network/INetworkService.cs @@ -1,4 +1,4 @@ -namespace salesbook.Shared.Core.Interface; +namespace salesbook.Shared.Core.Interface.System.Network; public interface INetworkService { diff --git a/salesbook.Shared/Core/Interface/System/Notification/IShinyNotificationManager.cs b/salesbook.Shared/Core/Interface/System/Notification/IShinyNotificationManager.cs new file mode 100644 index 0000000..ec8454b --- /dev/null +++ b/salesbook.Shared/Core/Interface/System/Notification/IShinyNotificationManager.cs @@ -0,0 +1,6 @@ +namespace salesbook.Shared.Core.Interface.System.Notification; + +public interface IShinyNotificationManager +{ + Task RequestAccess(); +} \ No newline at end of file diff --git a/salesbook.Shared/Core/Messages/Notification/NewPushNotificationMessage.cs b/salesbook.Shared/Core/Messages/Notification/NewPushNotificationMessage.cs new file mode 100644 index 0000000..2c3ad99 --- /dev/null +++ b/salesbook.Shared/Core/Messages/Notification/NewPushNotificationMessage.cs @@ -0,0 +1,6 @@ +using CommunityToolkit.Mvvm.Messaging.Messages; +using salesbook.Shared.Core.Entity; + +namespace salesbook.Shared.Core.Messages.Notification; + +public class NewPushNotificationMessage(WtbNotification value) : ValueChangedMessage(value); \ No newline at end of file diff --git a/salesbook.Shared/Core/Messages/Notification/NewPushNotificationService.cs b/salesbook.Shared/Core/Messages/Notification/NewPushNotificationService.cs new file mode 100644 index 0000000..d345d7f --- /dev/null +++ b/salesbook.Shared/Core/Messages/Notification/NewPushNotificationService.cs @@ -0,0 +1,14 @@ +using CommunityToolkit.Mvvm.Messaging; +using salesbook.Shared.Core.Entity; + +namespace salesbook.Shared.Core.Messages.Notification; + +public class NewPushNotificationService +{ + public event Action? OnNotificationReceived; + + public NewPushNotificationService(IMessenger messenger) + { + messenger.Register(this, (_, o) => { OnNotificationReceived?.Invoke(o.Value); }); + } +} \ No newline at end of file diff --git a/salesbook.Shared/Core/Services/IntegryApiService.cs b/salesbook.Shared/Core/Services/IntegryApiService.cs index c138b68..4a3d943 100644 --- a/salesbook.Shared/Core/Services/IntegryApiService.cs +++ b/salesbook.Shared/Core/Services/IntegryApiService.cs @@ -7,6 +7,7 @@ using salesbook.Shared.Core.Entity; using salesbook.Shared.Core.Interface; using System.Net.Http.Headers; using salesbook.Shared.Core.Dto.Contact; +using salesbook.Shared.Core.Interface.IntegryApi; namespace salesbook.Shared.Core.Services; diff --git a/salesbook.Shared/Core/Services/IntegryNotificationRestClient.cs b/salesbook.Shared/Core/Services/IntegryNotificationRestClient.cs new file mode 100644 index 0000000..519c337 --- /dev/null +++ b/salesbook.Shared/Core/Services/IntegryNotificationRestClient.cs @@ -0,0 +1,33 @@ +using IntegryApiClient.Core.Domain.Abstraction.Contracts.Account; +using IntegryApiClient.Core.Domain.RestClient.Contacts; +using salesbook.Shared.Core.Dto; +using salesbook.Shared.Core.Entity; +using salesbook.Shared.Core.Interface.IntegryApi; + +namespace salesbook.Shared.Core.Services; + +public class IntegryNotificationRestClient( + IUserSession userSession, + IIntegryApiRestClient integryApiRestClient) : IIntegryNotificationRestClient +{ + public Task> Get() + { + var queryParams = new Dictionary + { + { "mode", "ENABLED" }, + { "forUser", userSession.User.Username } + }; + + return integryApiRestClient.Get>("notification", queryParams)!; + } + + public Task MarkAsRead(long id) => + integryApiRestClient.Post("notification/read", + new ReadNotificationRequestDTO { NotificationId = id, Username = userSession.User.Username })!; + + public Task Delete(long id) => + integryApiRestClient.Delete($"notification/{id}", null); + + public Task DeleteAll() => + integryApiRestClient.Delete($"notification/all/{userSession.User.Username}", null); +} \ No newline at end of file diff --git a/salesbook.Shared/salesbook.Shared.csproj b/salesbook.Shared/salesbook.Shared.csproj index b9d0f7f..86765d8 100644 --- a/salesbook.Shared/salesbook.Shared.csproj +++ b/salesbook.Shared/salesbook.Shared.csproj @@ -24,7 +24,7 @@ - + diff --git a/salesbook.Shared/wwwroot/css/app.css b/salesbook.Shared/wwwroot/css/app.css index 076314e..a48eeeb 100644 --- a/salesbook.Shared/wwwroot/css/app.css +++ b/salesbook.Shared/wwwroot/css/app.css @@ -22,8 +22,12 @@ a, .btn-link { color: inherit; } -/*ServicesIsDown" : "SystemOk" : "NetworkKo*/ +article { + display: flex; + flex-direction: column; +} +/*ServicesIsDown" : "SystemOk" : "NetworkKo*/ .Connection { padding: 0 .75rem; font-weight: 700; @@ -58,6 +62,8 @@ a, .btn-link { transform: translateY(-35px); } +.page > .Connection.Hide ~ main .bottom-sheet-container.show { bottom: -35px; } + .btn-primary { color: #fff; background-color: var(--primary-color); diff --git a/salesbook.Shared/wwwroot/js/notifications.js b/salesbook.Shared/wwwroot/js/notifications.js new file mode 100644 index 0000000..6766874 --- /dev/null +++ b/salesbook.Shared/wwwroot/js/notifications.js @@ -0,0 +1,168 @@ +const FIRST_THRESHOLD = 80; +const SECOND_THRESHOLD = 160; +const CLOSE_THRESHOLD = 40; +const MAX_SWIPE = 200; + +let dotnetHelper; + +window.initNotifications = (dotnetRef) => { + dotnetHelper = dotnetRef; + document.querySelectorAll('.row').forEach(initRow); +}; + +function initRow(row) { + const card = row.querySelector('.notification-card'); + const btnTrash = row.querySelector('.trash-btn'); + const btnRead = row.querySelector('.read-btn'); + + const behindRight = row.querySelector('.behind-right'); // cestino + const behindLeft = row.querySelector('.behind-left'); // mark as read + + let startX = 0, currentX = 0, dragging = false; + let open = null; // "left", "right" oppure null + + // inizializza nascosti + behindRight.style.visibility = "hidden"; + behindLeft.style.visibility = "hidden"; + + // funzione di utilità → controlla se c'è unread-dot + function canMarkAsRead() { + return row.querySelector('.unread-dot') !== null; + } + + card.addEventListener('pointerdown', (e) => { + if (e.pointerType === 'mouse' && e.button !== 0) return; + dragging = true; + startX = e.clientX; + card.setPointerCapture(e.pointerId); + card.style.transition = 'none'; + }); + + card.addEventListener('pointermove', (e) => { + if (!dragging) return; + + const dx = e.clientX - startX; + + if (dx > 0 && !canMarkAsRead() && !open) { + currentX = 0; + return; // niente movimento + } + + let translate = dx; + + if (!open) { + translate = Math.max(-MAX_SWIPE, Math.min(MAX_SWIPE, dx)); + } else if (open === "left") { + translate = Math.min(MAX_SWIPE, FIRST_THRESHOLD + dx); + } else if (open === "right") { + translate = Math.max(-MAX_SWIPE, -FIRST_THRESHOLD + dx); + } + + currentX = translate; + card.style.transform = `translateX(${translate}px)`; + + // mostra/nascondi i behind in tempo reale + if (currentX < 0) { + behindRight.style.visibility = "visible"; // cestino + behindLeft.style.visibility = "hidden"; + } else if (currentX > 0) { + behindLeft.style.visibility = "visible"; // mark as read + behindRight.style.visibility = "hidden"; + } else { + behindLeft.style.visibility = "hidden"; + behindRight.style.visibility = "hidden"; + } + }); + + function endDrag() { + if (!dragging) return; + dragging = false; + card.style.transition = 'transform .2s ease'; + + // blocca swipe destro se non consentito + if (currentX > 0 && !canMarkAsRead() && !open) { + card.style.transform = 'translateX(0)'; + currentX = 0; + open = null; + behindRight.style.visibility = "hidden"; + behindLeft.style.visibility = "hidden"; + return; + } + + // Swipe a sinistra → elimina + if (!open && currentX < 0) { + if (currentX < -SECOND_THRESHOLD) { + card.style.transform = `translateX(-${MAX_SWIPE}px)`; + setTimeout(() => removeRow(row), 200); + } else if (currentX < -FIRST_THRESHOLD) { + card.style.transform = `translateX(-${FIRST_THRESHOLD}px)`; + open = "right"; + } else { + card.style.transform = 'translateX(0)'; + open = null; + behindRight.style.visibility = "hidden"; + behindLeft.style.visibility = "hidden"; + } + } + // Swipe a destra → mark as read SOLO se consentito + else if (!open && currentX > 0) { + if (currentX > SECOND_THRESHOLD) { + card.style.transform = `translateX(${MAX_SWIPE}px)`; + setTimeout(() => markAsRead(row), 200); + } else if (currentX > FIRST_THRESHOLD) { + card.style.transform = `translateX(${FIRST_THRESHOLD}px)`; + open = "left"; + } else { + card.style.transform = 'translateX(0)'; + open = null; + behindRight.style.visibility = "hidden"; + behindLeft.style.visibility = "hidden"; + } + } + // Se già aperta, gestisci chiusura + else { + if (open === "right" && currentX > -FIRST_THRESHOLD + CLOSE_THRESHOLD) { + card.style.transform = 'translateX(0)'; + open = null; + behindRight.style.visibility = "hidden"; + behindLeft.style.visibility = "hidden"; + } else if (open === "left" && currentX < FIRST_THRESHOLD - CLOSE_THRESHOLD) { + card.style.transform = 'translateX(0)'; + open = null; + behindRight.style.visibility = "hidden"; + behindLeft.style.visibility = "hidden"; + } else if (open) { + card.style.transform = `translateX(${open === "right" ? -FIRST_THRESHOLD : FIRST_THRESHOLD}px)`; + } + } + } + + card.addEventListener('pointerup', endDrag); + card.addEventListener('pointercancel', endDrag); + + btnTrash.addEventListener('click', () => removeRow(row)); + btnRead.addEventListener('click', () => markAsRead(row)); +} + +function removeRow(row) { + //collapseAndRemove(row); + dotnetHelper.invokeMethodAsync('Delete', row.id); +} + +function markAsRead(row) { + //collapseAndRemove(row); + dotnetHelper.invokeMethodAsync('MarkAsRead', row.id); +} + +function collapseAndRemove(row) { + const h = row.getBoundingClientRect().height; + row.style.height = h + 'px'; + row.classList.add('collapsing'); + requestAnimationFrame(() => { + row.style.opacity = '0'; + row.style.marginTop = '0'; + row.style.marginBottom = '0'; + row.style.height = '0'; + }); + setTimeout(() => row.remove(), 220); +} diff --git a/salesbook.Web/Core/Services/NetworkService.cs b/salesbook.Web/Core/Services/NetworkService.cs index 64b4044..16d9938 100644 --- a/salesbook.Web/Core/Services/NetworkService.cs +++ b/salesbook.Web/Core/Services/NetworkService.cs @@ -1,4 +1,5 @@ using salesbook.Shared.Core.Interface; +using salesbook.Shared.Core.Interface.System.Network; namespace salesbook.Web.Core.Services; diff --git a/salesbook.Web/Program.cs b/salesbook.Web/Program.cs index cbb65dc..fc2b24a 100644 --- a/salesbook.Web/Program.cs +++ b/salesbook.Web/Program.cs @@ -5,6 +5,8 @@ using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using MudBlazor.Services; using salesbook.Shared.Components; using salesbook.Shared.Core.Interface; +using salesbook.Shared.Core.Interface.IntegryApi; +using salesbook.Shared.Core.Interface.System.Network; using salesbook.Shared.Core.Services; using salesbook.Web.Core.Services; diff --git a/salesbook.Web/salesbook.Web.csproj b/salesbook.Web/salesbook.Web.csproj index 45301e1..78e0477 100644 --- a/salesbook.Web/salesbook.Web.csproj +++ b/salesbook.Web/salesbook.Web.csproj @@ -16,7 +16,7 @@ - +