From 068723f31f9cf6969cb987a01b610517d5a901cd Mon Sep 17 00:00:00 2001 From: MarcoE Date: Wed, 30 Jul 2025 18:27:24 +0200 Subject: [PATCH] Implementata gestione allegati --- .../Core/Services/AttachedService.cs | 65 +++++++++++ salesbook.Maui/MauiProgram.cs | 1 + .../Platforms/Android/AndroidManifest.xml | 7 +- salesbook.Maui/Platforms/iOS/Info.plist | 10 ++ .../Components/Layout/HeaderLayout.razor | 84 ++++++++------ .../SingleElements/Modal/ActivityForm.razor | 103 ++++++++++++++++-- .../Modal/ActivityForm.razor.css | 5 + .../SingleElements/Modal/AddAttached.razor | 80 ++++++++++++++ .../Modal/AddAttached.razor.css | 7 ++ salesbook.Shared/Core/Dto/ActivityDTO.cs | 2 + salesbook.Shared/Core/Dto/AttachedDTO.cs | 24 ++++ salesbook.Shared/Core/Dto/PositionDTO.cs | 18 +++ salesbook.Shared/Core/Helpers/ModalHelpers.cs | 16 +++ .../Core/Interface/IAttachedService.cs | 10 ++ .../Core/Interface/IIntegryApiService.cs | 6 + .../Core/Services/IntegryApiService.cs | 37 +++++-- 16 files changed, 422 insertions(+), 53 deletions(-) create mode 100644 salesbook.Maui/Core/Services/AttachedService.cs create mode 100644 salesbook.Shared/Components/SingleElements/Modal/AddAttached.razor create mode 100644 salesbook.Shared/Components/SingleElements/Modal/AddAttached.razor.css create mode 100644 salesbook.Shared/Core/Dto/AttachedDTO.cs create mode 100644 salesbook.Shared/Core/Dto/PositionDTO.cs create mode 100644 salesbook.Shared/Core/Interface/IAttachedService.cs diff --git a/salesbook.Maui/Core/Services/AttachedService.cs b/salesbook.Maui/Core/Services/AttachedService.cs new file mode 100644 index 0000000..fa432d3 --- /dev/null +++ b/salesbook.Maui/Core/Services/AttachedService.cs @@ -0,0 +1,65 @@ +using salesbook.Shared.Core.Dto; +using salesbook.Shared.Core.Interface; + +namespace salesbook.Maui.Core.Services; + +public class AttachedService : IAttachedService +{ + public async Task SelectImage() + { + var perm = await Permissions.RequestAsync(); + if (perm != PermissionStatus.Granted) return null; + + var result = await FilePicker.PickAsync(new PickOptions + { + PickerTitle = "Scegli un'immagine", + FileTypes = FilePickerFileType.Images + }); + + return result is null ? null : await ConvertToDto(result, AttachedDTO.TypeAttached.Image); + } + + public async Task SelectFile() + { + var perm = await Permissions.RequestAsync(); + if (perm != PermissionStatus.Granted) return null; + + var result = await FilePicker.PickAsync(); + + return result is null ? null : await ConvertToDto(result, AttachedDTO.TypeAttached.Document); + } + + public async Task SelectPosition() + { + var perm = await Permissions.RequestAsync(); + if (perm != PermissionStatus.Granted) return null; + + var loc = await Geolocation.GetLastKnownLocationAsync(); + if (loc is null) return null; + + return new AttachedDTO + { + Name = "Posizione attuale", + Lat = loc.Latitude, + Lng = loc.Longitude, + Type = AttachedDTO.TypeAttached.Position + }; + } + + private static async Task ConvertToDto(FileResult file, AttachedDTO.TypeAttached type) + { + var stream = await file.OpenReadAsync(); + using var ms = new MemoryStream(); + await stream.CopyToAsync(ms); + + return new AttachedDTO + { + Name = file.FileName, + Path = file.FullPath, + MimeType = file.ContentType, + DimensionBytes= ms.Length, + FileContent = ms.ToArray(), + Type = type + }; + } +} \ No newline at end of file diff --git a/salesbook.Maui/MauiProgram.cs b/salesbook.Maui/MauiProgram.cs index 5e78a85..b3c22f1 100644 --- a/salesbook.Maui/MauiProgram.cs +++ b/salesbook.Maui/MauiProgram.cs @@ -70,6 +70,7 @@ namespace salesbook.Maui #endif builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); return builder.Build(); diff --git a/salesbook.Maui/Platforms/Android/AndroidManifest.xml b/salesbook.Maui/Platforms/Android/AndroidManifest.xml index 9698f04..ddb278b 100644 --- a/salesbook.Maui/Platforms/Android/AndroidManifest.xml +++ b/salesbook.Maui/Platforms/Android/AndroidManifest.xml @@ -1,6 +1,9 @@  - + - + + + + \ No newline at end of file diff --git a/salesbook.Maui/Platforms/iOS/Info.plist b/salesbook.Maui/Platforms/iOS/Info.plist index ee8e215..a094b3e 100644 --- a/salesbook.Maui/Platforms/iOS/Info.plist +++ b/salesbook.Maui/Platforms/iOS/Info.plist @@ -35,5 +35,15 @@ NSAllowsArbitraryLoads + + NSLocationWhenInUseUsageDescription + L'app utilizza la tua posizione per allegarla alle attività. + + NSPhotoLibraryUsageDescription + Consente di selezionare immagini da allegare alle attività. + + NSPhotoLibraryAddUsageDescription + Permette all'app di salvare file o immagini nella tua libreria fotografica se necessario. + diff --git a/salesbook.Shared/Components/Layout/HeaderLayout.razor b/salesbook.Shared/Components/Layout/HeaderLayout.razor index 19f494c..6a01ac3 100644 --- a/salesbook.Shared/Components/Layout/HeaderLayout.razor +++ b/salesbook.Shared/Components/Layout/HeaderLayout.razor @@ -2,49 +2,61 @@
- @if (Back) + @if (!SmallHeader) { -
- - @BackTo - -
- } - -

@Title

- -
- @if (LabelSave.IsNullOrEmpty()) + @if (Back) { - @if (ShowFilter) - { - - } +
+ + @BackTo + +
+ } - @* @if (ShowCalendarToggle) +

@Title

+ +
+ @if (LabelSave.IsNullOrEmpty()) + { + @if (ShowFilter) + { + + } + + @* @if (ShowCalendarToggle) { } *@ - @if (ShowProfile) - { - + @if (ShowProfile) + { + + } } - } - else - { - - @LabelSave - - } -
+ else + { + + @LabelSave + + } +
+ } + else + { +
+ + @Title + + +
+ }
@@ -66,6 +78,8 @@ [Parameter] public bool ShowCalendarToggle { get; set; } [Parameter] public EventCallback OnCalendarToggle { get; set; } + [Parameter] public bool SmallHeader { get; set; } + protected override void OnParametersSet() { Back = !Back ? !Back && Cancel : Back; diff --git a/salesbook.Shared/Components/SingleElements/Modal/ActivityForm.razor b/salesbook.Shared/Components/SingleElements/Modal/ActivityForm.razor index 574884a..08f1bda 100644 --- a/salesbook.Shared/Components/SingleElements/Modal/ActivityForm.razor +++ b/salesbook.Shared/Components/SingleElements/Modal/ActivityForm.razor @@ -12,6 +12,7 @@ @inject INetworkService NetworkService @inject IIntegryApiService IntegryApiService @inject IMessenger Messenger +@inject IDialogService Dialog @@ -116,10 +117,33 @@
- - @if (!IsNew) - { -
+ +
+ @if (!AttachedList.IsNullOrEmpty()) + { + foreach (var item in AttachedList!.Select((p, index) => new { p, index })) + { + + @item.p.Name + + } + } +
+ +
+ + Aggiungi allegati + + + @if (!IsNew) + { +
+ Elimina -
- } + } +
- + Confermi la cancellazione dell'attività corrente? @@ -154,7 +178,7 @@ - + Vuoi creare un promemoria? @@ -173,7 +197,7 @@ - + @code { [CascadingParameter] private IMudDialogInstance MudDialog { get; set; } @@ -207,6 +231,9 @@ private MudMessageBox ConfirmDelete { get; set; } private MudMessageBox ConfirmMemo { get; set; } + //Attached + private List? AttachedList { get; set; } + protected override async Task OnInitializedAsync() { Snackbar.Configuration.PositionClass = Defaults.Classes.Position.TopCenter; @@ -242,6 +269,8 @@ VisibleOverlay = true; StateHasChanged(); + await SavePosition(); + var response = await IntegryApiService.SaveActivity(ActivityModel); if (response == null) @@ -251,6 +280,8 @@ await ManageData.InsertOrUpdate(newActivity); + await SaveAttached(newActivity.ActivityId); + SuccessAnimation = true; StateHasChanged(); @@ -259,6 +290,40 @@ MudDialog.Close(newActivity); } + private async Task SavePosition() + { + if (AttachedList != null) + { + foreach (var attached in AttachedList) + { + if (attached.Type != AttachedDTO.TypeAttached.Position) continue; + + var position = new PositionDTO + { + Description = attached.Description, + Lat = attached.Lat, + Lng = attached.Lng + }; + + ActivityModel.Position = await IntegryApiService.SavePosition(position); + } + } + } + + private async Task SaveAttached(string activityId) + { + if (AttachedList != null) + { + foreach (var attached in AttachedList) + { + if (attached.FileContent is not null && attached.Type != AttachedDTO.TypeAttached.Position) + { + await IntegryApiService.UploadFile(activityId, attached.FileContent, attached.Name); + } + } + } + } + private bool CheckPreSave() { Snackbar.Clear(); @@ -395,4 +460,24 @@ Messenger.Send(new CopyActivityMessage(activityCopy)); } + private async Task OpenAddAttached() + { + var result = await ModalHelpers.OpenAddAttached(Dialog); + + if (result is { Canceled: false, Data: not null } && result.Data.GetType() == typeof(AttachedDTO)) + { + AttachedList ??= []; + AttachedList.Add((AttachedDTO)result.Data); + } + } + + private void OnRemoveAttached(int index) + { + if (AttachedList is null || index < 0 || index >= AttachedList.Count) + return; + + AttachedList.RemoveAt(index); + StateHasChanged(); + } + } \ No newline at end of file diff --git a/salesbook.Shared/Components/SingleElements/Modal/ActivityForm.razor.css b/salesbook.Shared/Components/SingleElements/Modal/ActivityForm.razor.css index cb50e2e..e17b77c 100644 --- a/salesbook.Shared/Components/SingleElements/Modal/ActivityForm.razor.css +++ b/salesbook.Shared/Components/SingleElements/Modal/ActivityForm.razor.css @@ -1,3 +1,8 @@ +.container-chip-attached { + width: 100%; + margin-bottom: 1rem; +} + .container-button { background: var(--mud-palette-background-gray) !important; box-shadow: unset; diff --git a/salesbook.Shared/Components/SingleElements/Modal/AddAttached.razor b/salesbook.Shared/Components/SingleElements/Modal/AddAttached.razor new file mode 100644 index 0000000..0bb7aa2 --- /dev/null +++ b/salesbook.Shared/Components/SingleElements/Modal/AddAttached.razor @@ -0,0 +1,80 @@ +@using salesbook.Shared.Core.Dto +@using salesbook.Shared.Components.Layout +@using salesbook.Shared.Core.Interface +@using salesbook.Shared.Components.Layout.Overlay +@inject IAttachedService AttachedService + + + + + +
+ + + + + +
+
+
+ + + +@code { + [CascadingParameter] private IMudDialogInstance MudDialog { get; set; } + + //Overlay for save + private bool VisibleOverlay { get; set; } + private bool SuccessAnimation { get; set; } + + private AttachedDTO? Attached { get; set; } + + protected override async Task OnInitializedAsync() + { + Snackbar.Configuration.PositionClass = Defaults.Classes.Position.TopCenter; + + _ = LoadData(); + } + + private async Task LoadData() + { + } + + private async Task Save() + { + VisibleOverlay = true; + StateHasChanged(); + + SuccessAnimation = true; + StateHasChanged(); + + await Task.Delay(1250); + + MudDialog.Close(); + } + + private async Task OnImage() + { + Attached = await AttachedService.SelectImage(); + MudDialog.Close(Attached); + } + + private async Task OnFile() + { + Attached = await AttachedService.SelectFile(); + MudDialog.Close(Attached); + } + + private async Task OnPosition() + { + Attached = await AttachedService.SelectPosition(); + MudDialog.Close(Attached); + } + +} \ No newline at end of file diff --git a/salesbook.Shared/Components/SingleElements/Modal/AddAttached.razor.css b/salesbook.Shared/Components/SingleElements/Modal/AddAttached.razor.css new file mode 100644 index 0000000..6eeaddd --- /dev/null +++ b/salesbook.Shared/Components/SingleElements/Modal/AddAttached.razor.css @@ -0,0 +1,7 @@ +.content.attached { + display: flex; + flex-direction: column; + gap: 2rem; + padding: 1rem; + height: unset; +} diff --git a/salesbook.Shared/Core/Dto/ActivityDTO.cs b/salesbook.Shared/Core/Dto/ActivityDTO.cs index fcb34ba..bd5f262 100644 --- a/salesbook.Shared/Core/Dto/ActivityDTO.cs +++ b/salesbook.Shared/Core/Dto/ActivityDTO.cs @@ -11,6 +11,8 @@ public class ActivityDTO : StbActivity public bool Complete { get; set; } public bool Deleted { get; set; } + + public PositionDTO? Position { get; set; } public ActivityDTO Clone() { diff --git a/salesbook.Shared/Core/Dto/AttachedDTO.cs b/salesbook.Shared/Core/Dto/AttachedDTO.cs new file mode 100644 index 0000000..74e91b3 --- /dev/null +++ b/salesbook.Shared/Core/Dto/AttachedDTO.cs @@ -0,0 +1,24 @@ +namespace salesbook.Shared.Core.Dto; + +public class AttachedDTO +{ + public string Name { get; set; } + public string? Description { get; set; } + + public string? MimeType { get; set; } + public long? DimensionBytes { get; set; } + public string? Path { get; set; } + public byte[]? FileContent { get; set; } + + public double? Lat { get; set; } + public double? Lng { get; set; } + + public TypeAttached Type { get; set; } + + public enum TypeAttached + { + Image, + Document, + Position + } +} \ No newline at end of file diff --git a/salesbook.Shared/Core/Dto/PositionDTO.cs b/salesbook.Shared/Core/Dto/PositionDTO.cs new file mode 100644 index 0000000..e283c6d --- /dev/null +++ b/salesbook.Shared/Core/Dto/PositionDTO.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace salesbook.Shared.Core.Dto; + +public class PositionDTO +{ + [JsonPropertyName("id")] + public long? Id { get; set; } + + [JsonPropertyName("description")] + public string? Description { get; set; } + + [JsonPropertyName("lat")] + public double? Lat { get; set; } + + [JsonPropertyName("lng")] + public double? Lng { get; set; } +} \ No newline at end of file diff --git a/salesbook.Shared/Core/Helpers/ModalHelpers.cs b/salesbook.Shared/Core/Helpers/ModalHelpers.cs index 0a619ea..a5a8be3 100644 --- a/salesbook.Shared/Core/Helpers/ModalHelpers.cs +++ b/salesbook.Shared/Core/Helpers/ModalHelpers.cs @@ -63,4 +63,20 @@ public class ModalHelpers return await modal.Result; } + + public static async Task OpenAddAttached(IDialogService dialog) + { + var modal = await dialog.ShowAsync( + "Add attached", + new DialogParameters(), + new DialogOptions + { + FullScreen = false, + CloseButton = false, + NoHeader = true + } + ); + + return await modal.Result; + } } \ No newline at end of file diff --git a/salesbook.Shared/Core/Interface/IAttachedService.cs b/salesbook.Shared/Core/Interface/IAttachedService.cs new file mode 100644 index 0000000..6897b19 --- /dev/null +++ b/salesbook.Shared/Core/Interface/IAttachedService.cs @@ -0,0 +1,10 @@ +using salesbook.Shared.Core.Dto; + +namespace salesbook.Shared.Core.Interface; + +public interface IAttachedService +{ + Task SelectImage(); + Task SelectFile(); + Task SelectPosition(); +} \ No newline at end of file diff --git a/salesbook.Shared/Core/Interface/IIntegryApiService.cs b/salesbook.Shared/Core/Interface/IIntegryApiService.cs index baf522a..8b2f02b 100644 --- a/salesbook.Shared/Core/Interface/IIntegryApiService.cs +++ b/salesbook.Shared/Core/Interface/IIntegryApiService.cs @@ -16,7 +16,13 @@ public interface IIntegryApiService Task?> SaveActivity(ActivityDTO activity); Task SaveContact(CRMCreateContactRequestDTO request); Task CheckVat(CheckVatRequestDTO request); + + Task UploadFile(string id, byte[] file, string fileName); + //Position + Task SavePosition(PositionDTO position); + Task RetrievePosition(string id); + //Google Task?> Geocode(string address); Task?> AutoCompleteAddress(string address, string language, string uuid); diff --git a/salesbook.Shared/Core/Services/IntegryApiService.cs b/salesbook.Shared/Core/Services/IntegryApiService.cs index c77661e..dc898bd 100644 --- a/salesbook.Shared/Core/Services/IntegryApiService.cs +++ b/salesbook.Shared/Core/Services/IntegryApiService.cs @@ -1,9 +1,10 @@ -using System.Xml; -using IntegryApiClient.Core.Domain.Abstraction.Contracts.Account; +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; +using System.Net.Http.Headers; +using System.Reflection.Metadata.Ecma335; namespace salesbook.Shared.Core.Services; @@ -79,8 +80,8 @@ public class IntegryApiService(IIntegryApiRestClient integryApiRestClient, IUser { var queryParams = new Dictionary { - {"address", address}, - {"retrieveAll", true} + { "address", address }, + { "retrieveAll", true } }; return integryApiRestClient.Get>("geocode", queryParams); @@ -90,9 +91,9 @@ public class IntegryApiService(IIntegryApiRestClient integryApiRestClient, IUser { var queryParams = new Dictionary { - {"address", address}, - {"language", language}, - {"uuid", uuid} + { "address", address }, + { "language", language }, + { "uuid", uuid } }; return integryApiRestClient.Get>("google/places/autoCompleteAddress", queryParams); @@ -108,4 +109,26 @@ public class IntegryApiService(IIntegryApiRestClient integryApiRestClient, IUser return integryApiRestClient.Get("google/places/placeDetails", queryParams); } + + public Task UploadFile(string activityId, byte[] file, string fileName) + { + var queryParams = new Dictionary { { "activityId", activityId } }; + + using var content = new MultipartFormDataContent(); + var fileContent = new ByteArrayContent(file); + fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse("multipart/form-data"); + content.Add(fileContent, "files", fileName); + + return integryApiRestClient.Post($"uploadStbActivityFileAttachment", content, queryParams); + } + + public Task SavePosition(PositionDTO position) => + integryApiRestClient.Post("savePosition", position)!; + + public Task RetrievePosition(string id) + { + var queryParams = new Dictionary { { "id", id } }; + + return integryApiRestClient.Get("retrievePosition", queryParams)!; + } } \ No newline at end of file