Gestiti allegati nel form

This commit is contained in:
2026-02-20 15:29:32 +01:00
parent eef5055bfa
commit b39b7ba751
26 changed files with 512 additions and 263 deletions

View File

@@ -1,10 +1,14 @@
using SteUp.Shared.Core.Dto; using SteUp.Shared.Core.Dto;
using SteUp.Shared.Core.Helpers;
using SteUp.Shared.Core.Interface.System; using SteUp.Shared.Core.Interface.System;
namespace SteUp.Maui.Core.Services; namespace SteUp.Maui.Core.Services;
public class AttachedService : IAttachedService public class AttachedService : IAttachedService
{ {
private static string AttachedRoot =>
Path.Combine(FileSystem.CacheDirectory, "attached");
public async Task<AttachedDto?> SelectImageFromCamera() public async Task<AttachedDto?> SelectImageFromCamera()
{ {
var cameraPerm = await Permissions.RequestAsync<Permissions.Camera>(); var cameraPerm = await Permissions.RequestAsync<Permissions.Camera>();
@@ -18,6 +22,7 @@ public class AttachedService : IAttachedService
try try
{ {
result = await MediaPicker.Default.CapturePhotoAsync(); result = await MediaPicker.Default.CapturePhotoAsync();
result?.FileName = $"img_{DateTime.Now:ddMMyyy_hhmmss}{result.FileName[result.FileName.IndexOf('.')..]}";
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -29,17 +34,16 @@ public class AttachedService : IAttachedService
return result is null ? null : await ConvertToDto(result, AttachedDto.TypeAttached.Image); return result is null ? null : await ConvertToDto(result, AttachedDto.TypeAttached.Image);
} }
public async Task<AttachedDto?> SelectImageFromGallery() public async Task<List<AttachedDto>?> SelectImageFromGallery()
{ {
List<FileResult>? resultList;
var storagePerm = await Permissions.RequestAsync<Permissions.StorageRead>(); var storagePerm = await Permissions.RequestAsync<Permissions.StorageRead>();
if (storagePerm != PermissionStatus.Granted) if (storagePerm != PermissionStatus.Granted)
return null; return null;
FileResult? result;
try try
{ {
result = await MediaPicker.Default.PickPhotoAsync(); resultList = await MediaPicker.Default.PickPhotosAsync();
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -48,7 +52,15 @@ public class AttachedService : IAttachedService
return null; return null;
} }
return result is null ? null : await ConvertToDto(result, AttachedDto.TypeAttached.Image); if (resultList.IsNullOrEmpty()) return null;
List<AttachedDto> returnList = [];
foreach (var fileResult in resultList)
{
returnList.Add(await ConvertToDto(fileResult, AttachedDto.TypeAttached.Image));
}
return returnList;
} }
private static async Task<AttachedDto> ConvertToDto(FileResult file, AttachedDto.TypeAttached type) private static async Task<AttachedDto> ConvertToDto(FileResult file, AttachedDto.TypeAttached type)
@@ -86,6 +98,15 @@ public class AttachedService : IAttachedService
return filePath; return filePath;
} }
public Task CleanTempStorageAsync(CancellationToken ct = default)
{
return Task.Run(() =>
{
if (Directory.Exists(AttachedRoot))
Directory.Delete(AttachedRoot, true);
}, ct);
}
public Task OpenFile(string fileName, string filePath) public Task OpenFile(string fileName, string filePath)
{ {
#if IOS #if IOS
@@ -98,4 +119,31 @@ public class AttachedService : IAttachedService
}); });
#endif #endif
} }
public async Task<(string originalUrl, string thumbUrl)> SaveAndCreateThumbAsync(
byte[] bytes, string fileName, CancellationToken ct = default)
{
Directory.CreateDirectory(AttachedRoot);
var id = Guid.NewGuid().ToString("N");
var safeName = SanitizeFileName(fileName);
var originalFile = $"{id}_{safeName}";
var thumbFile = $"{id}_thumb.jpg";
var originalPath = Path.Combine(AttachedRoot, originalFile);
await File.WriteAllBytesAsync(originalPath, bytes, ct);
var thumbPath = Path.Combine(AttachedRoot, thumbFile);
await ImageThumb.CreateThumbnailAsync(originalPath, thumbPath, maxSide: 320, quality: 70, ct);
return ($"https://localfiles/attached/{originalFile}",
$"https://localfiles/attached/{thumbFile}");
}
private static string SanitizeFileName(string fileName)
{
var name = Path.GetFileName(fileName);
return Path.GetInvalidFileNameChars().Aggregate(name, (current, c) => current.Replace(c, '_'));
}
} }

View File

@@ -0,0 +1,52 @@
using SkiaSharp;
namespace SteUp.Maui.Core.Services;
public static class ImageThumb
{
public static async Task CreateThumbnailAsync(
string inputPath,
string outputPath,
int maxSide = 320,
int quality = 70,
CancellationToken ct = default)
{
// Leggi bytes (meglio in async)
var data = await File.ReadAllBytesAsync(inputPath, ct);
using var codec = SKCodec.Create(new SKMemoryStream(data));
if (codec is null)
throw new InvalidOperationException("Formato immagine non supportato o file corrotto.");
// Decodifica
var info = codec.Info;
using var bitmap = SKBitmap.Decode(codec);
if (bitmap is null)
throw new InvalidOperationException("Impossibile decodificare l'immagine.");
// Calcola resize mantenendo aspect ratio
var w = bitmap.Width;
var h = bitmap.Height;
if (w <= 0 || h <= 0) throw new InvalidOperationException("Dimensioni immagine non valide.");
var scale = (float)maxSide / Math.Max(w, h);
if (scale > 1f) scale = 1f; // non ingrandire
var newW = Math.Max(1, (int)Math.Round(w * scale));
var newH = Math.Max(1, (int)Math.Round(h * scale));
using var resized = bitmap.Resize(new SKImageInfo(newW, newH), SKFilterQuality.Medium);
if (resized is null)
throw new InvalidOperationException("Resize fallito.");
using var image = SKImage.FromBitmap(resized);
using var encoded = image.Encode(SKEncodedImageFormat.Jpeg, quality);
Directory.CreateDirectory(Path.GetDirectoryName(outputPath)!);
await using var fs = File.Open(outputPath, FileMode.Create, FileAccess.Write, FileShare.None);
encoded.SaveTo(fs);
await fs.FlushAsync(ct);
}
}

View File

@@ -6,7 +6,7 @@
SafeAreaEdges="All" SafeAreaEdges="All"
BackgroundColor="{DynamicResource PageBackgroundColor}"> BackgroundColor="{DynamicResource PageBackgroundColor}">
<BlazorWebView HostPage="wwwroot/index.html"> <BlazorWebView x:Name="BlazorWebView" HostPage="wwwroot/index.html">
<BlazorWebView.RootComponents> <BlazorWebView.RootComponents>
<RootComponent Selector="#app" ComponentType="{x:Type shared:Components.Routes}" /> <RootComponent Selector="#app" ComponentType="{x:Type shared:Components.Routes}" />
</BlazorWebView.RootComponents> </BlazorWebView.RootComponents>

View File

@@ -1,10 +1,59 @@
namespace SteUp.Maui namespace SteUp.Maui;
public partial class MainPage : ContentPage
{ {
public partial class MainPage : ContentPage private static readonly string AttachedDir =
Path.Combine(FileSystem.CacheDirectory, "attached");
private const string Prefix = "https://localfiles/attached/";
public MainPage()
{ {
public MainPage() InitializeComponent();
{
InitializeComponent(); Directory.CreateDirectory(AttachedDir);
}
BlazorWebView.WebResourceRequested += BlazorWebView_WebResourceRequested;
} }
}
private static void BlazorWebView_WebResourceRequested(object? sender, WebViewWebResourceRequestedEventArgs e)
{
var uri = e.Uri.ToString();
if (string.IsNullOrWhiteSpace(uri) ||
!uri.StartsWith(Prefix, StringComparison.OrdinalIgnoreCase))
return;
var fileName = uri[Prefix.Length..];
fileName = fileName.Replace("\\", "/");
if (fileName.Contains("..") || fileName.Contains('/'))
{
e.Handled = true;
e.SetResponse(400, "Bad Request");
return;
}
var fullPath = Path.Combine(AttachedDir, fileName);
if (!File.Exists(fullPath))
{
e.Handled = true;
e.SetResponse(404, "Not Found");
return;
}
e.Handled = true;
e.SetResponse(200, "OK", GetContentType(fullPath), File.OpenRead(fullPath));
}
private static string GetContentType(string path)
{
return Path.GetExtension(path).ToLowerInvariant() switch
{
".png" => "image/png",
".jpg" or ".jpeg" => "image/jpeg",
".webp" => "image/webp",
_ => "application/octet-stream"
};
}
}

View File

@@ -1,5 +1,5 @@
<?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" package="it.integry.SteUp">
<application <application
android:allowBackup="true" android:allowBackup="true"
android:icon="@mipmap/appicon" android:icon="@mipmap/appicon"
@@ -10,4 +10,19 @@
<!-- Rete --> <!-- Rete -->
<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" />
<!-- Fotocamera -->
<uses-permission android:name="android.permission.CAMERA" />
<!-- Storage / Media -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- Android 10+ -->
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
<!-- Android 13+ -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
</manifest> </manifest>

View File

@@ -121,6 +121,7 @@
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="10.0.40" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="10.0.40" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="10.0.3" /> <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="10.0.3" />
<PackageReference Include="Sentry.Maui" Version="6.1.0" /> <PackageReference Include="Sentry.Maui" Version="6.1.0" />
<PackageReference Include="SkiaSharp" Version="3.119.2" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -0,0 +1,6 @@
@page "/not-found"
@using SteUp.Shared.Components.Layout
@layout MainLayout
<h3>Not Found</h3>
<p>Sorry, the content you are looking for does not exist.</p>

View File

@@ -4,7 +4,7 @@
<ErrorBoundary @ref="ErrorBoundary"> <ErrorBoundary @ref="ErrorBoundary">
<ChildContent> <ChildContent>
<CascadingAuthenticationState> <CascadingAuthenticationState>
<Router AppAssembly="@typeof(Routes).Assembly"> <Router AppAssembly="@typeof(Routes).Assembly" NotFoundPage="typeof(Pages.NotFound)">
<Found Context="routeData"> <Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)"> <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)">
<Authorizing> <Authorizing>
@@ -23,12 +23,6 @@
</AuthorizeRouteView> </AuthorizeRouteView>
<FocusOnNavigate RouteData="@routeData" Selector="h1"/> <FocusOnNavigate RouteData="@routeData" Selector="h1"/>
</Found> </Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView>
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router> </Router>
</CascadingAuthenticationState> </CascadingAuthenticationState>
</ChildContent> </ChildContent>
@@ -44,7 +38,7 @@
@code { @code {
private ErrorBoundary? ErrorBoundary { get; set; } private ErrorBoundary? ErrorBoundary { get; set; }
private ExceptionModal ExceptionModal { get; set; } private ExceptionModal ExceptionModal { get; set; } = null!;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {

View File

@@ -92,7 +92,7 @@
var puntoVendita = SteupDataService.PuntiVenditaList.Find(x => var puntoVendita = SteupDataService.PuntiVenditaList.Find(x =>
x.CodMdep != null && x.CodMdep.EqualsIgnoreCase(Ispezione.CodMdep) x.CodMdep != null && x.CodMdep.EqualsIgnoreCase(Ispezione.CodMdep)
); );
await Task.Delay(500); await Task.Delay(250);
PuntoVendita = puntoVendita ?? throw new Exception("Punto vendita non trovato"); PuntoVendita = puntoVendita ?? throw new Exception("Punto vendita non trovato");
OnLoading = false; OnLoading = false;

View File

@@ -0,0 +1,21 @@
<MudMessageBox @ref="_confirmSave" Title="Attenzione!" CancelText="Non salvare">
<MessageContent>
Sono state apportate delle modifiche. Vuoi salvarle prima di continuare?
</MessageContent>
<YesButton>
<MudButton Size="Size.Small" Variant="Variant.Filled" Color="Color.Primary">
Salva
</MudButton>
</YesButton>
</MudMessageBox>
@code
{
private MudMessageBox? _confirmSave;
public async Task<bool?> ShowAsync()
{
if (_confirmSave == null) return null;
return await _confirmSave.ShowAsync();
}
}

View File

@@ -5,117 +5,47 @@
<MudDialog Class="disable-safe-area"> <MudDialog Class="disable-safe-area">
<DialogContent> <DialogContent>
<HeaderLayout SmallHeader="true" Cancel="true" OnCancel="@(() => MudDialog.Cancel())" Title="@TitleModal"/> <HeaderLayout SmallHeader="true" Cancel="true" OnCancel="@(() => MudDialog.Cancel())" Title="Aggiungi allegati"/>
@if (RequireNewName) <div style="margin-bottom: 1rem;" class="content attached">
{ <MudFab Size="Size.Small" Color="Color.Primary"
<MudTextField @bind-Value="NewName" Class="px-3" Variant="Variant.Outlined"/> StartIcon="@Icons.Material.Rounded.CameraAlt"
} Label="Camera" OnClick="@OnCamera"/>
else
{
<div style="margin-bottom: 1rem;" class="content attached">
<MudFab Size="Size.Small" Color="Color.Primary"
StartIcon="@Icons.Material.Rounded.CameraAlt"
Label="Camera" OnClick="@OnCamera"/>
<MudFab Size="Size.Small" Color="Color.Primary" <MudFab Size="Size.Small" Color="Color.Primary"
StartIcon="@Icons.Material.Rounded.Image" StartIcon="@Icons.Material.Rounded.Image"
Label="Galleria" OnClick="@OnGallery"/> Label="Galleria" OnClick="@OnGallery"/>
</div> </div>
}
</DialogContent> </DialogContent>
<DialogActions>
@if (RequireNewName)
{
<MudButton Disabled="NewName.IsNullOrEmpty()" Class="my-3" Size="Size.Small" Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Rounded.Check" OnClick="@OnNewName">
Salva
</MudButton>
}
</DialogActions>
</MudDialog> </MudDialog>
@code { @code {
[CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = null!; [CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = null!;
[Parameter] public bool CanAddPosition { get; set; } [Parameter] public bool CanAddPosition { get; set; }
private AttachedDto? Attached { get; set; }
private bool _requireNewName; private List<AttachedDto>? Attached { get; set; }
private bool RequireNewName
{
get => _requireNewName;
set
{
_requireNewName = value;
TitleModal = _requireNewName ? "Nome allegato" : "Aggiungi allegati";
StateHasChanged();
}
}
private string TitleModal { get; set; } = "Aggiungi allegati";
private string? _newName;
private string? NewName
{
get => _newName;
set
{
_newName = value;
StateHasChanged();
}
}
protected override void OnInitialized() protected override void OnInitialized()
{ {
RequireNewName = false;
Snackbar.Configuration.PositionClass = Defaults.Classes.Position.TopCenter; Snackbar.Configuration.PositionClass = Defaults.Classes.Position.TopCenter;
} }
private async Task OnCamera() private async Task OnCamera()
{ {
Attached = await AttachedService.SelectImageFromCamera(); var selectImageFromCamera = await AttachedService.SelectImageFromCamera();
if (Attached != null) if (selectImageFromCamera != null)
{ {
RequireNewName = true; Attached ??= [];
StateHasChanged(); Attached.Add(selectImageFromCamera);
MudDialog.Close(Attached);
} }
} }
private async Task OnGallery() private async Task OnGallery()
{ {
Attached = await AttachedService.SelectImageFromGallery(); Attached = await AttachedService.SelectImageFromGallery();
if (Attached != null) if (Attached != null) MudDialog.Close(Attached);
{
RequireNewName = true;
StateHasChanged();
}
} }
private void OnNewName()
{
if (Attached != null)
{
switch (Attached.Type)
{
case AttachedDto.TypeAttached.Image:
{
var extension = Path.GetExtension(Attached.Name);
Attached.Name = NewName! + extension;
break;
}
default:
throw new ArgumentOutOfRangeException();
}
}
MudDialog.Close(Attached);
}
} }

View File

@@ -1,135 +1,126 @@
@using SteUp.Shared.Components.Layout @using SteUp.Shared.Components.Layout
@using SteUp.Shared.Components.Layout.Overlay @using SteUp.Shared.Components.Layout.Overlay
@using SteUp.Shared.Components.Layout.Spinner
@using SteUp.Shared.Components.SingleElements.Card.ModalForm @using SteUp.Shared.Components.SingleElements.Card.ModalForm
@using SteUp.Shared.Components.SingleElements.MessageBox
@using SteUp.Shared.Core.Dto @using SteUp.Shared.Core.Dto
@using SteUp.Shared.Core.Entities @using SteUp.Shared.Core.Entities
@using SteUp.Shared.Core.Interface.IntegryApi @using SteUp.Shared.Core.Interface.IntegryApi
@using SteUp.Shared.Core.Interface.System
@using SteUp.Shared.Core.Interface.System.Network @using SteUp.Shared.Core.Interface.System.Network
@inject INetworkService NetworkService @inject INetworkService NetworkService
@inject IDialogService Dialog @inject IDialogService Dialog
@inject IIntegryApiService IntegryApiService @inject IIntegryApiService IntegryApiService
@inject IAttachedService AttachedService
<MudDialog Class="customDialog-form"> <MudDialog Class="customDialog-form">
<DialogContent> <DialogContent>
<MudForm @ref="_form">
<HeaderLayout Cancel="true" OnCancel="@(() => MudDialog.Cancel())" LabelSave="@LabelSave" <HeaderLayout Cancel="true" OnCancel="@Cancel" LabelSave="@LabelSave"
OnSave="Save" Title="Scheda"/> OnSave="Save" Title="Scheda"/>
<div class="content"> <div class="content">
<CardFormModal Title="Reparto" Loading="SteupDataService.Reparti.IsNullOrEmpty()"> <CardFormModal Title="Reparto" Loading="SteupDataService.Reparti.IsNullOrEmpty()">
<MudSelectExtended ReadOnly="IsView" T="JtbFasiDto?" Variant="Variant.Text" <MudSelectExtended ReadOnly="IsView" T="JtbFasiDto?" Variant="Variant.Text"
@bind-Value="Scheda.Reparto" ToStringFunc="@(x => x?.Descrizione)" @bind-Value="Scheda.Reparto" ToStringFunc="@(x => x?.Descrizione)"
@bind-Value:after="OnAfterChangeValue"> @bind-Value:after="OnAfterChangeValue" Required="true"
@foreach (var fasi in SteupDataService.Reparti) RequiredError="Reparto obbligatorio">
{ @foreach (var fasi in SteupDataService.Reparti)
<MudSelectItemExtended Class="custom-item-select" Value="@fasi"> {
@fasi.Descrizione <MudSelectItemExtended Class="custom-item-select" Value="@fasi">
</MudSelectItemExtended> @fasi.Descrizione
} </MudSelectItemExtended>
</MudSelectExtended> }
</CardFormModal> </MudSelectExtended>
</CardFormModal>
<CardFormModal Title="Motivo" Loading="SteupDataService.TipiAttività.IsNullOrEmpty()"> <CardFormModal Title="Motivo" Loading="SteupDataService.TipiAttività.IsNullOrEmpty()">
<MudSelectExtended ReadOnly="IsView" T="string?" Variant="Variant.Text" <MudSelectExtended ReadOnly="@(IsView || Scheda.CodJfas.IsNullOrEmpty())" T="string?"
@bind-Value="Scheda.ActivityTypeId" @bind-Value:after="OnAfterChangeValue"> Variant="Variant.Text"
@foreach (var type in SteupDataService.TipiAttività) @bind-Value="Scheda.ActivityTypeId" @bind-Value:after="OnAfterChangeValue"
{ Required="true" RequiredError="Motivo obbligatorio">
<MudSelectItemExtended Class="custom-item-select" @foreach (var type in SteupDataService.TipiAttività.Where(x => x.CodJfas.EqualsIgnoreCase(Scheda.CodJfas!)))
Value="@type.ActivityTypeId">@type.ActivityTypeId</MudSelectItemExtended> {
} <MudSelectItemExtended Class="custom-item-select"
</MudSelectExtended> Value="@type.ActivityTypeId">@type.ActivityTypeId</MudSelectItemExtended>
</CardFormModal> }
</MudSelectExtended>
</CardFormModal>
@* <div class="container-chip-attached"> *@ @if (!AttachedList.IsNullOrEmpty())
@* @if (!AttachedList.IsNullOrEmpty()) *@ {
@* { *@ <div class="container-attached">
@* foreach (var item in AttachedList!.Select((p, index) => new { p, index })) *@ <div class="scroll-attached">
@* { *@ @foreach (var item in AttachedList!.Select((p, index) => new { p, index }))
@* if (item.p.Type == AttachedDTO.TypeAttached.Position) *@ {
@* { *@ <MudCard>
@* <MudChip T="string" Icon="@Icons.Material.Rounded.LocationOn" Color="Color.Success" *@ @if (!item.p.ThumbPath.IsNullOrEmpty())
@* OnClick="@(() => OpenPosition(item.p))" *@ {
@* OnClose="@(() => OnRemoveAttached(item.index))"> *@ <MudCardMedia Image="@item.p.ThumbPath" Height="100"/>
@* @item.p.Description *@ }
@* </MudChip> *@ <MudCardContent Class="image_card">
@* } *@ <MudText Typo="Typo.subtitle1"><b>@item.p.Name</b></MudText>
@* else *@ <MudIconButton Variant="Variant.Outlined" Icon="@Icons.Material.Rounded.Close"
@* { *@ Size="Size.Small" Color="Color.Error"
@* <MudChip T="string" Color="Color.Default" OnClick="@(() => OpenAttached(item.p))" *@ OnClick="@(() => OnRemoveAttached(item.index))"/>
@* OnClose="@(() => OnRemoveAttached(item.index))"> *@ </MudCardContent>
@* @item.p.Name *@ </MudCard>
@* </MudChip> *@ }
@* } *@ </div>
@* } *@ </div>
@* } *@ }
@* *@
@* @if (!IsLoading) *@ @if (!IsView)
@* { *@ {
@* if (ActivityFileList != null) *@ <div class="container-button">
@* { *@ <MudButton Class="button-settings green-icon"
@* foreach (var file in ActivityFileList) *@ FullWidth="true"
@* { *@ StartIcon="@Icons.Material.Rounded.AttachFile"
@* <MudChip T="string" OnClick="@(() => OpenAttached(file.FileName))" *@ Size="Size.Medium"
@* OnClose="@(() => DeleteAttach(file))" Color="Color.Default"> *@ OnClick="@OpenAddAttached"
@* @file.FileName *@ Variant="Variant.Outlined">
@* </MudChip> *@ Aggiungi foto
@* } *@ </MudButton>
@* } *@ </div>
@* } *@ }
@* else *@
@* { *@ <CardFormModal Title="Scadenza">
@* <MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="my-7"/> *@ <MudSelectExtended FullWidth="true" ReadOnly="@IsView" T="int" Variant="Variant.Text"
@* } *@ @bind-Value="@Scheda.Scadenza" @bind-Value:after="OnAfterChangeValue">
@* </div> *@ <MudSelectItemExtended Class="custom-item-select" Text="24H" Value="24"/>
<MudSelectItemExtended Class="custom-item-select" Text="1 Settimana" Value="168"/>
<MudSelectItemExtended Class="custom-item-select" Text="1 Mese" Value="730"/>
<MudSelectItemExtended Class="custom-item-select" Text="2 Mesi" Value="1460"/>
</MudSelectExtended>
</CardFormModal>
<CardFormModal Title="Responsabile">
<MudTextField FullWidth="true" ReadOnly="IsView" T="string?" Variant="Variant.Text"
@bind-Value="Scheda.Responsabile" @bind-Value:after="OnAfterChangeValue"
DebounceInterval="500" OnDebounceIntervalElapsed="OnAfterChangeValue"/>
</CardFormModal>
<CardFormModal Title="Note">
<MudTextField ReadOnly="IsView" T="string?" Variant="Variant.Text" Lines="3"
@bind-Value="Scheda.Note" @bind-Value:after="OnAfterChangeValue"
DebounceInterval="500" OnDebounceIntervalElapsed="OnAfterChangeValue"/>
</CardFormModal>
@if (!IsView)
{
<div class="container-button"> <div class="container-button">
<MudButton Class="button-settings green-icon" <MudButton Class="button-settings blue-icon"
FullWidth="true" FullWidth="true"
StartIcon="@Icons.Material.Rounded.AttachFile" StartIcon="@Icons.Material.Rounded.Description"
Size="Size.Medium" Size="Size.Medium"
OnClick="@OpenAddAttached" OnClick="@SuggestActivityDescription"
Variant="Variant.Outlined"> Variant="Variant.Outlined">
Aggiungi foto Suggerisci note descrittive
</MudButton> </MudButton>
</div> </div>
}
<CardFormModal Title="Scadenza">
<MudSelectExtended FullWidth="true" ReadOnly="@IsView" T="int" Variant="Variant.Text"
@bind-Value="@Scheda.Scadenza" @bind-Value:after="OnAfterChangeValue">
<MudSelectItemExtended Class="custom-item-select" Text="24H" Value="24"/>
<MudSelectItemExtended Class="custom-item-select" Text="1 Settimana" Value="168"/>
<MudSelectItemExtended Class="custom-item-select" Text="1 Mese" Value="730"/>
<MudSelectItemExtended Class="custom-item-select" Text="2 Mesi" Value="1460"/>
</MudSelectExtended>
</CardFormModal>
<CardFormModal Title="Responsabile">
<MudTextField FullWidth="true" ReadOnly="IsView" T="string?" Variant="Variant.Text"
@bind-Value="Scheda.Responsabile" @bind-Value:after="OnAfterChangeValue"
DebounceInterval="500" OnDebounceIntervalElapsed="OnAfterChangeValue"/>
</CardFormModal>
<CardFormModal Title="Note">
<MudTextField ReadOnly="IsView" T="string?" Variant="Variant.Text" Lines="3"
@bind-Value="Scheda.Note" @bind-Value:after="OnAfterChangeValue"
DebounceInterval="500" OnDebounceIntervalElapsed="OnAfterChangeValue"/>
</CardFormModal>
<div class="container-button">
<MudButton Class="button-settings blue-icon"
FullWidth="true"
StartIcon="@Icons.Material.Rounded.Description"
Size="Size.Medium"
OnClick="@SuggestActivityDescription"
Variant="Variant.Outlined">
Suggerisci note descrittive
</MudButton>
</div> </div>
</div> </MudForm>
<ConfirmUpdateActivity @ref="_confirmUpdateMessage"/>
</DialogContent> </DialogContent>
</MudDialog> </MudDialog>
@@ -148,10 +139,17 @@
private bool VisibleOverlay { get; set; } private bool VisibleOverlay { get; set; }
private bool SuccessAnimation { get; set; } private bool SuccessAnimation { get; set; }
private string? LabelSave { get; set; } private ConfirmUpdateActivity _confirmUpdateMessage = null!;
private MudForm _form = null!;
protected override async Task OnInitializedAsync() private string? LabelSave { get; set; }
private bool IsDirty { get; set; }
private Scheda _originalScheda = null!;
private List<AttachedDto>? AttachedList { get; set; }
protected override void OnInitialized()
{ {
_originalScheda = Scheda.Clone();
Snackbar.Configuration.PositionClass = Defaults.Classes.Position.TopCenter; Snackbar.Configuration.PositionClass = Defaults.Classes.Position.TopCenter;
} }
@@ -159,30 +157,129 @@
{ {
} }
private async Task OpenAddAttached() private async Task Cancel()
{ {
var result = await ModalHelper.OpenAddAttached(Dialog); if (await CheckSavePreAction())
{
// if (result is { Canceled: false, Data: not null } && result.Data.GetType() == typeof(AttachedDTO)) await AttachedService.CleanTempStorageAsync();
// { MudDialog.Cancel();
// var attached = (AttachedDTO)result.Data; }
//
// if (attached.Type == AttachedDTO.TypeAttached.Position)
// CanAddPosition = false;
//
// AttachedList ??= [];
// AttachedList.Add(attached);
// }
} }
#region Form
private void OnAfterChangeValue() private void OnAfterChangeValue()
{ {
if (!IsNew) RecalcDirty();
LabelSave = "Aggiorna";
StateHasChanged(); StateHasChanged();
} }
private void RecalcDirty()
{
IsDirty = !ValueComparer.AreEqual(Scheda, _originalScheda);
if (IsDirty) LabelSave = !IsNew ? "Aggiorna" : "Salva";
else LabelSave = null;
}
private static class ValueComparer
{
public static bool AreEqual(Scheda? a, Scheda? b)
{
if (a is null || b is null) return a == b;
return
a.CodJfas == b.CodJfas &&
a.DescrizioneReparto == b.DescrizioneReparto &&
a.ActivityTypeId == b.ActivityTypeId &&
a.Note == b.Note &&
a.Responsabile == b.Responsabile &&
a.Scadenza == b.Scadenza;
}
}
private async Task<bool> CheckSavePreAction()
{
if (!IsDirty) return true;
var resul = await _confirmUpdateMessage.ShowAsync();
if (resul is not true) return true;
VisibleOverlay = true;
StateHasChanged();
await Submit();
VisibleOverlay = false;
StateHasChanged();
return false;
}
private async Task Submit()
{
await _form.Validate();
if (_form.IsValid) await Save();
}
#endregion
#region File
private async Task OpenAddAttached()
{
var result = await ModalHelper.OpenAddAttached(Dialog);
if (result is not { Canceled: false, Data: List<AttachedDto> attachedList }) return;
VisibleOverlay = true;
await InvokeAsync(StateHasChanged);
await Task.Yield();
// prepara placeholder in UI subito (così vedi le card con spinner)
AttachedList ??= [];
foreach (var a in attachedList)
AttachedList.Add(new AttachedDto { Name = a.Name, MimeType = a.MimeType, FileBytes = a.FileBytes });
await InvokeAsync(StateHasChanged);
// Processa in background e aggiorna UI man mano (o a blocchi)
_ = Task.Run(async () =>
{
for (var i = 0; i < attachedList.Count; i++)
{
var a = attachedList[i];
if (a.FileBytes is null || a.Name is null) continue;
var (origUrl, thumbUrl) = await AttachedService.SaveAndCreateThumbAsync(a.FileBytes, a.Name);
await InvokeAsync(() =>
{
var target = AttachedList![AttachedList.Count - attachedList.Count + i];
target.TempPath = origUrl;
target.ThumbPath = thumbUrl;
StateHasChanged();
});
}
await InvokeAsync(() =>
{
VisibleOverlay = false;
StateHasChanged();
});
});
}
private void OnRemoveAttached(int index)
{
if (AttachedList is null || index < 0 || index >= AttachedList.Count)
return;
AttachedList.RemoveAt(index);
StateHasChanged();
}
#endregion
private void SuggestActivityDescription() private void SuggestActivityDescription()
{ {
if (Scheda.ActivityTypeId == null) if (Scheda.ActivityTypeId == null)

View File

@@ -1,6 +1,21 @@
.container-chip-attached { .container-attached {
width: 100%; width: 100%;
margin-bottom: 1rem; }
.scroll-attached {
max-height: 185px;
overflow: auto;
display: flex;
gap: 1rem;
flex-direction: column;
padding: .5rem;
}
.container-attached ::deep .image_card {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px;
} }
.container-button { .container-button {

View File

@@ -11,5 +11,5 @@ public interface ISteupDataService
List<PuntoVenditaDto> PuntiVenditaList { get; } List<PuntoVenditaDto> PuntiVenditaList { get; }
InspectionPageState InspectionPageState { get; set; } InspectionPageState InspectionPageState { get; set; }
List<JtbFasiDto> Reparti { get; } List<JtbFasiDto> Reparti { get; }
List<StbActivityTypeDto> TipiAttività { get; } List<ActivityTypeDto> TipiAttività { get; }
} }

View File

@@ -42,5 +42,5 @@ public class SteupDataService(
public InspectionPageState InspectionPageState { get; set; } = new(); public InspectionPageState InspectionPageState { get; set; } = new();
public List<PuntoVenditaDto> PuntiVenditaList { get; private set; } = []; public List<PuntoVenditaDto> PuntiVenditaList { get; private set; } = [];
public List<JtbFasiDto> Reparti { get; private set; } = []; public List<JtbFasiDto> Reparti { get; private set; } = [];
public List<StbActivityTypeDto> TipiAttività { get; private set; } = []; public List<ActivityTypeDto> TipiAttività { get; private set; } = [];
} }

View File

@@ -0,0 +1,12 @@
using System.Text.Json.Serialization;
namespace SteUp.Shared.Core.Dto;
public class ActivityTypeDto
{
[JsonPropertyName("activityTypeId")]
public string ActivityTypeId { get; set; } = string.Empty;
[JsonPropertyName("codJfas")]
public string CodJfas { get; set; } = string.Empty;
}

View File

@@ -10,6 +10,9 @@ public class AttachedDto
public string? Path { get; set; } public string? Path { get; set; }
public byte[]? FileBytes { get; set; } public byte[]? FileBytes { get; set; }
public string? TempPath { get; set; }
public string? ThumbPath { get; set; }
public Stream? FileContent => public Stream? FileContent =>
FileBytes is null ? null : new MemoryStream(FileBytes); FileBytes is null ? null : new MemoryStream(FileBytes);

View File

@@ -1,9 +0,0 @@
using System.Text.Json.Serialization;
namespace SteUp.Shared.Core.Dto;
public class StbActivityTypeDto
{
[JsonPropertyName("activityTypeId")]
public string? ActivityTypeId { get; set; }
}

View File

@@ -0,0 +1,9 @@
namespace SteUp.Shared.Core.Entities;
public class EntityBase<T>
{
public T Clone()
{
return (T)MemberwiseClone();
}
}

View File

@@ -3,7 +3,7 @@ using SteUp.Shared.Core.Enum;
namespace SteUp.Shared.Core.Entities; namespace SteUp.Shared.Core.Entities;
public class Ispezione public class Ispezione : EntityBase<Ispezione>
{ {
[Required] [Required]
public string CodMdep { get; set; } = string.Empty; public string CodMdep { get; set; } = string.Empty;

View File

@@ -4,7 +4,7 @@ using SteUp.Shared.Core.Dto;
namespace SteUp.Shared.Core.Entities; namespace SteUp.Shared.Core.Entities;
public class Scheda public class Scheda : EntityBase<Scheda>
{ {
[Key] [Key]
public int Id { get; set; } public int Id { get; set; }
@@ -28,24 +28,24 @@ public class Scheda
{ {
get get
{ {
if (_reparto == null && CodJfas != null) if (field == null && CodJfas != null)
{ {
_reparto = new JtbFasiDto field = new JtbFasiDto
{ {
CodJfas = CodJfas, CodJfas = CodJfas,
Descrizione = DescrizioneReparto Descrizione = DescrizioneReparto
}; };
} }
return _reparto;
return field;
} }
set set
{ {
_reparto = value; field = value;
if (value == null) return; if (value == null) return;
CodJfas = value.CodJfas; CodJfas = value.CodJfas;
DescrizioneReparto = value.Descrizione; DescrizioneReparto = value.Descrizione;
} }
} }
private JtbFasiDto? _reparto;
} }

View File

@@ -4,7 +4,7 @@ using SteUp.Shared.Core.Dto;
namespace SteUp.Shared.Core.Helpers; namespace SteUp.Shared.Core.Helpers;
public class ModalHelper public abstract class ModalHelper
{ {
public static async Task<DialogResult?> OpenSelectShop(IDialogService dialog) public static async Task<DialogResult?> OpenSelectShop(IDialogService dialog)
{ {

View File

@@ -7,5 +7,5 @@ public interface IIntegrySteupService
//Retrieve //Retrieve
Task<List<PuntoVenditaDto>> RetrievePuntiVendita(); Task<List<PuntoVenditaDto>> RetrievePuntiVendita();
Task<List<JtbFasiDto>> RetrieveReparti(); Task<List<JtbFasiDto>> RetrieveReparti();
Task<List<StbActivityTypeDto>> RetrieveActivityType(); Task<List<ActivityTypeDto>> RetrieveActivityType();
} }

View File

@@ -5,8 +5,11 @@ namespace SteUp.Shared.Core.Interface.System;
public interface IAttachedService public interface IAttachedService
{ {
Task<AttachedDto?> SelectImageFromCamera(); Task<AttachedDto?> SelectImageFromCamera();
Task<AttachedDto?> SelectImageFromGallery(); Task<List<AttachedDto>?> SelectImageFromGallery();
Task<string> SaveToTempStorage(Stream file, string fileName, CancellationToken ct = default); Task<string> SaveToTempStorage(Stream file, string fileName, CancellationToken ct = default);
Task CleanTempStorageAsync(CancellationToken ct = default);
Task OpenFile(string fileName, string filePath); Task OpenFile(string fileName, string filePath);
Task<(string originalUrl, string thumbUrl)> SaveAndCreateThumbAsync(byte[] bytes, string fileName, CancellationToken ct = default);
} }

View File

@@ -16,8 +16,8 @@ public class IntegrySteupService(IIntegryApiRestClient integryApiRestClient) : I
public Task<List<JtbFasiDto>> RetrieveReparti() => public Task<List<JtbFasiDto>> RetrieveReparti() =>
integryApiRestClient.AuthorizedGet<List<JtbFasiDto>>($"{BaseRequest}/retrieveReparti")!; integryApiRestClient.AuthorizedGet<List<JtbFasiDto>>($"{BaseRequest}/retrieveReparti")!;
public Task<List<StbActivityTypeDto>> RetrieveActivityType() => public Task<List<ActivityTypeDto>> RetrieveActivityType() =>
integryApiRestClient.AuthorizedGet<List<StbActivityTypeDto>>($"{BaseRequest}/retrieveActivityType")!; integryApiRestClient.AuthorizedGet<List<ActivityTypeDto>>($"{BaseRequest}/retrieveActivityType")!;
#endregion #endregion
} }

View File

@@ -21,7 +21,10 @@
height: calc(100vh - (.6rem + 40px)); height: calc(100vh - (.6rem + 40px));
overflow: auto; overflow: auto;
gap: 1.5rem; gap: 1.5rem;
padding: 0 .75rem 2rem 75rem !important; padding-top: unset !important;
padding-bottom: 2rem !important;
padding-left: .75rem !important;
padding-right: .75rem !important;
} }
@supports (-webkit-touch-callout: none) { @supports (-webkit-touch-callout: none) {