Implementata gestione allegati

This commit is contained in:
2025-07-30 18:27:24 +02:00
parent 8ebc6e3b8f
commit 068723f31f
16 changed files with 422 additions and 53 deletions

View File

@@ -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<AttachedDTO?> SelectImage()
{
var perm = await Permissions.RequestAsync<Permissions.Photos>();
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<AttachedDTO?> SelectFile()
{
var perm = await Permissions.RequestAsync<Permissions.StorageRead>();
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<AttachedDTO?> SelectPosition()
{
var perm = await Permissions.RequestAsync<Permissions.LocationWhenInUse>();
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<AttachedDTO> 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
};
}
}

View File

@@ -70,6 +70,7 @@ namespace salesbook.Maui
#endif #endif
builder.Services.AddSingleton<IFormFactor, FormFactor>(); builder.Services.AddSingleton<IFormFactor, FormFactor>();
builder.Services.AddSingleton<IAttachedService, AttachedService>();
builder.Services.AddSingleton<LocalDbService>(); builder.Services.AddSingleton<LocalDbService>();
return builder.Build(); return builder.Build();

View File

@@ -1,6 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:allowBackup="true" android:icon="@mipmap/appicon" android:usesCleartextTraffic="true" android:supportsRtl="true"></application> <application android:allowBackup="true" android:icon="@mipmap/appicon" android:usesCleartextTraffic="true" android:supportsRtl="true"></application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
</manifest> </manifest>

View File

@@ -35,5 +35,15 @@
<key>NSAllowsArbitraryLoads</key> <key>NSAllowsArbitraryLoads</key>
<true/> <true/>
</dict> </dict>
<key>NSLocationWhenInUseUsageDescription</key>
<string>L'app utilizza la tua posizione per allegarla alle attività.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Consente di selezionare immagini da allegare alle attività.</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>Permette all'app di salvare file o immagini nella tua libreria fotografica se necessario.</string>
</dict> </dict>
</plist> </plist>

View File

@@ -2,49 +2,61 @@
<div class="@(Back ? "" : "container") header"> <div class="@(Back ? "" : "container") header">
<div class="header-content @(Back ? "with-back" : "no-back")"> <div class="header-content @(Back ? "with-back" : "no-back")">
@if (Back) @if (!SmallHeader)
{ {
<div class="left-section"> @if (Back)
<MudButton StartIcon="@(!Cancel ? Icons.Material.Outlined.ArrowBackIosNew : "")"
OnClick="GoBack"
Color="Color.Info"
Style="text-transform: none"
Variant="Variant.Text">
@BackTo
</MudButton>
</div>
}
<h3 class="page-title">@Title</h3>
<div class="right-section">
@if (LabelSave.IsNullOrEmpty())
{ {
@if (ShowFilter) <div class="left-section">
{ <MudButton StartIcon="@(!Cancel ? Icons.Material.Outlined.ArrowBackIosNew : "")"
<MudIconButton OnClick="OnFilterToggle" Icon="@Icons.Material.Outlined.FilterAlt"/> OnClick="GoBack"
} Color="Color.Info"
Style="text-transform: none"
Variant="Variant.Text">
@BackTo
</MudButton>
</div>
}
@* @if (ShowCalendarToggle) <h3 class="page-title">@Title</h3>
<div class="right-section">
@if (LabelSave.IsNullOrEmpty())
{
@if (ShowFilter)
{
<MudIconButton OnClick="OnFilterToggle" Icon="@Icons.Material.Outlined.FilterAlt"/>
}
@* @if (ShowCalendarToggle)
{ {
<MudIconButton OnClick="OnCalendarToggle" Icon="@Icons.Material.Filled.CalendarMonth" Color="Color.Dark"/> <MudIconButton OnClick="OnCalendarToggle" Icon="@Icons.Material.Filled.CalendarMonth" Color="Color.Dark"/>
} *@ } *@
@if (ShowProfile) @if (ShowProfile)
{ {
<MudIconButton Class="user" OnClick="OpenPersonalInfo" Icon="@Icons.Material.Filled.Person"/> <MudIconButton Class="user" OnClick="OpenPersonalInfo" Icon="@Icons.Material.Filled.Person"/>
}
} }
} else
else {
{ <MudButton OnClick="OnSave"
<MudButton OnClick="OnSave" Color="Color.Info"
Color="Color.Info" Style="text-transform: none"
Style="text-transform: none" Variant="Variant.Text">
Variant="Variant.Text"> @LabelSave
@LabelSave </MudButton>
</MudButton> }
} </div>
</div> }
else
{
<div class="title">
<MudText Typo="Typo.h6">
<b>@Title</b>
</MudText>
<MudIconButton Icon="@Icons.Material.Filled.Close" OnClick="() => GoBack()" />
</div>
}
</div> </div>
</div> </div>
@@ -66,6 +78,8 @@
[Parameter] public bool ShowCalendarToggle { get; set; } [Parameter] public bool ShowCalendarToggle { get; set; }
[Parameter] public EventCallback OnCalendarToggle { get; set; } [Parameter] public EventCallback OnCalendarToggle { get; set; }
[Parameter] public bool SmallHeader { get; set; }
protected override void OnParametersSet() protected override void OnParametersSet()
{ {
Back = !Back ? !Back && Cancel : Back; Back = !Back ? !Back && Cancel : Back;

View File

@@ -12,6 +12,7 @@
@inject INetworkService NetworkService @inject INetworkService NetworkService
@inject IIntegryApiService IntegryApiService @inject IIntegryApiService IntegryApiService
@inject IMessenger Messenger @inject IMessenger Messenger
@inject IDialogService Dialog
<MudDialog Class="customDialog-form"> <MudDialog Class="customDialog-form">
<DialogContent> <DialogContent>
@@ -117,9 +118,32 @@
<MudTextField ReadOnly="IsView" T="string?" Placeholder="Note" Variant="Variant.Text" Lines="4" @bind-Value="ActivityModel.Note" @bind-Value:after="OnAfterChangeValue" DebounceInterval="500" OnDebounceIntervalElapsed="OnAfterChangeValue"/> <MudTextField ReadOnly="IsView" T="string?" Placeholder="Note" Variant="Variant.Text" Lines="4" @bind-Value="ActivityModel.Note" @bind-Value:after="OnAfterChangeValue" DebounceInterval="500" OnDebounceIntervalElapsed="OnAfterChangeValue"/>
</div> </div>
@if (!IsNew) <div class="container-chip-attached">
{ @if (!AttachedList.IsNullOrEmpty())
<div class="container-button"> {
foreach (var item in AttachedList!.Select((p, index) => new { p, index }))
{
<MudChip T="string" Color="Color.Default" OnClose="() => OnRemoveAttached(item.index)">
@item.p.Name
</MudChip>
}
}
</div>
<div class="container-button">
<MudButton Class="button-settings green-icon"
FullWidth="true"
StartIcon="@Icons.Material.Rounded.AttachFile"
Size="Size.Medium"
OnClick="OpenAddAttached"
Variant="Variant.Outlined">
Aggiungi allegati
</MudButton>
@if (!IsNew)
{
<div class="divider"></div>
<MudButton Class="button-settings gray-icon" <MudButton Class="button-settings gray-icon"
FullWidth="true" FullWidth="true"
StartIcon="@Icons.Material.Filled.ContentCopy" StartIcon="@Icons.Material.Filled.ContentCopy"
@@ -139,8 +163,8 @@
Variant="Variant.Outlined"> Variant="Variant.Outlined">
Elimina Elimina
</MudButton> </MudButton>
</div> }
} </div>
</div> </div>
<MudMessageBox @ref="ConfirmDelete" Class="c-messageBox" Title="Attenzione!" CancelText="Annulla"> <MudMessageBox @ref="ConfirmDelete" Class="c-messageBox" Title="Attenzione!" CancelText="Annulla">
@@ -173,7 +197,7 @@
<SelectEsito @bind-IsSheetVisible="OpenEsito" @bind-ActivityModel="ActivityModel" @bind-ActivityModel:after="OnAfterChangeEsito"/> <SelectEsito @bind-IsSheetVisible="OpenEsito" @bind-ActivityModel="ActivityModel" @bind-ActivityModel:after="OnAfterChangeEsito"/>
<AddMemo @bind-IsSheetVisible="OpenAddMemo" /> <AddMemo @bind-IsSheetVisible="OpenAddMemo"/>
@code { @code {
[CascadingParameter] private IMudDialogInstance MudDialog { get; set; } [CascadingParameter] private IMudDialogInstance MudDialog { get; set; }
@@ -207,6 +231,9 @@
private MudMessageBox ConfirmDelete { get; set; } private MudMessageBox ConfirmDelete { get; set; }
private MudMessageBox ConfirmMemo { get; set; } private MudMessageBox ConfirmMemo { get; set; }
//Attached
private List<AttachedDTO>? AttachedList { get; set; }
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
Snackbar.Configuration.PositionClass = Defaults.Classes.Position.TopCenter; Snackbar.Configuration.PositionClass = Defaults.Classes.Position.TopCenter;
@@ -242,6 +269,8 @@
VisibleOverlay = true; VisibleOverlay = true;
StateHasChanged(); StateHasChanged();
await SavePosition();
var response = await IntegryApiService.SaveActivity(ActivityModel); var response = await IntegryApiService.SaveActivity(ActivityModel);
if (response == null) if (response == null)
@@ -251,6 +280,8 @@
await ManageData.InsertOrUpdate(newActivity); await ManageData.InsertOrUpdate(newActivity);
await SaveAttached(newActivity.ActivityId);
SuccessAnimation = true; SuccessAnimation = true;
StateHasChanged(); StateHasChanged();
@@ -259,6 +290,40 @@
MudDialog.Close(newActivity); 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() private bool CheckPreSave()
{ {
Snackbar.Clear(); Snackbar.Clear();
@@ -395,4 +460,24 @@
Messenger.Send(new CopyActivityMessage(activityCopy)); 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();
}
} }

View File

@@ -1,3 +1,8 @@
.container-chip-attached {
width: 100%;
margin-bottom: 1rem;
}
.container-button { .container-button {
background: var(--mud-palette-background-gray) !important; background: var(--mud-palette-background-gray) !important;
box-shadow: unset; box-shadow: unset;

View File

@@ -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
<MudDialog Class="customDialog-form">
<DialogContent>
<HeaderLayout ShowProfile="false" SmallHeader="true" Cancel="true" OnCancel="() => MudDialog.Cancel()" Title="Aggiungi allegati"/>
<div style="margin-bottom: 1rem;" class="content attached">
<MudFab Size="Size.Small" Color="Color.Primary"
StartIcon="@Icons.Material.Rounded.Image"
Label="Immagini" OnClick="OnImage"/>
<MudFab Size="Size.Small" Color="Color.Primary"
StartIcon="@Icons.Material.Rounded.InsertDriveFile"
Label="File" OnClick="OnFile"/>
<MudFab Size="Size.Small" Color="Color.Primary"
StartIcon="@Icons.Material.Rounded.AddLocationAlt"
Label="Posizione" OnClick="OnPosition"/>
</div>
</DialogContent>
</MudDialog>
<SaveOverlay VisibleOverlay="VisibleOverlay" SuccessAnimation="SuccessAnimation"/>
@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);
}
}

View File

@@ -0,0 +1,7 @@
.content.attached {
display: flex;
flex-direction: column;
gap: 2rem;
padding: 1rem;
height: unset;
}

View File

@@ -12,6 +12,8 @@ public class ActivityDTO : StbActivity
public bool Deleted { get; set; } public bool Deleted { get; set; }
public PositionDTO? Position { get; set; }
public ActivityDTO Clone() public ActivityDTO Clone()
{ {
return (ActivityDTO)MemberwiseClone(); return (ActivityDTO)MemberwiseClone();

View File

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

View File

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

View File

@@ -63,4 +63,20 @@ public class ModalHelpers
return await modal.Result; return await modal.Result;
} }
public static async Task<DialogResult?> OpenAddAttached(IDialogService dialog)
{
var modal = await dialog.ShowAsync<AddAttached>(
"Add attached",
new DialogParameters<AddAttached>(),
new DialogOptions
{
FullScreen = false,
CloseButton = false,
NoHeader = true
}
);
return await modal.Result;
}
} }

View File

@@ -0,0 +1,10 @@
using salesbook.Shared.Core.Dto;
namespace salesbook.Shared.Core.Interface;
public interface IAttachedService
{
Task<AttachedDTO?> SelectImage();
Task<AttachedDTO?> SelectFile();
Task<AttachedDTO?> SelectPosition();
}

View File

@@ -17,6 +17,12 @@ public interface IIntegryApiService
Task<CRMCreateContactResponseDTO?> SaveContact(CRMCreateContactRequestDTO request); Task<CRMCreateContactResponseDTO?> SaveContact(CRMCreateContactRequestDTO request);
Task<CheckVatResponseDTO> CheckVat(CheckVatRequestDTO request); Task<CheckVatResponseDTO> CheckVat(CheckVatRequestDTO request);
Task UploadFile(string id, byte[] file, string fileName);
//Position
Task<PositionDTO> SavePosition(PositionDTO position);
Task<PositionDTO> RetrievePosition(string id);
//Google //Google
Task<List<IndirizzoDTO>?> Geocode(string address); Task<List<IndirizzoDTO>?> Geocode(string address);
Task<List<AutoCompleteAddressDTO>?> AutoCompleteAddress(string address, string language, string uuid); Task<List<AutoCompleteAddressDTO>?> AutoCompleteAddress(string address, string language, string uuid);

View File

@@ -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 IntegryApiClient.Core.Domain.RestClient.Contacts;
using salesbook.Shared.Core.Dto; using salesbook.Shared.Core.Dto;
using salesbook.Shared.Core.Entity; using salesbook.Shared.Core.Entity;
using salesbook.Shared.Core.Interface; using salesbook.Shared.Core.Interface;
using System.Net.Http.Headers;
using System.Reflection.Metadata.Ecma335;
namespace salesbook.Shared.Core.Services; namespace salesbook.Shared.Core.Services;
@@ -79,8 +80,8 @@ public class IntegryApiService(IIntegryApiRestClient integryApiRestClient, IUser
{ {
var queryParams = new Dictionary<string, object> var queryParams = new Dictionary<string, object>
{ {
{"address", address}, { "address", address },
{"retrieveAll", true} { "retrieveAll", true }
}; };
return integryApiRestClient.Get<List<IndirizzoDTO>>("geocode", queryParams); return integryApiRestClient.Get<List<IndirizzoDTO>>("geocode", queryParams);
@@ -90,9 +91,9 @@ public class IntegryApiService(IIntegryApiRestClient integryApiRestClient, IUser
{ {
var queryParams = new Dictionary<string, object> var queryParams = new Dictionary<string, object>
{ {
{"address", address}, { "address", address },
{"language", language}, { "language", language },
{"uuid", uuid} { "uuid", uuid }
}; };
return integryApiRestClient.Get<List<AutoCompleteAddressDTO>>("google/places/autoCompleteAddress", queryParams); return integryApiRestClient.Get<List<AutoCompleteAddressDTO>>("google/places/autoCompleteAddress", queryParams);
@@ -108,4 +109,26 @@ public class IntegryApiService(IIntegryApiRestClient integryApiRestClient, IUser
return integryApiRestClient.Get<IndirizzoDTO>("google/places/placeDetails", queryParams); return integryApiRestClient.Get<IndirizzoDTO>("google/places/placeDetails", queryParams);
} }
public Task UploadFile(string activityId, byte[] file, string fileName)
{
var queryParams = new Dictionary<string, object> { { "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<object>($"uploadStbActivityFileAttachment", content, queryParams);
}
public Task<PositionDTO> SavePosition(PositionDTO position) =>
integryApiRestClient.Post<PositionDTO>("savePosition", position)!;
public Task<PositionDTO> RetrievePosition(string id)
{
var queryParams = new Dictionary<string, object> { { "id", id } };
return integryApiRestClient.Get<PositionDTO>("retrievePosition", queryParams)!;
}
} }