Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3b04702665 | |||
| 0174316bb7 | |||
| d076a4b77a | |||
| e3942d00cc | |||
| 92b60deac0 |
@@ -0,0 +1,157 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using SteUp.Data.LocalDb;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace SteUp.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDbContext))]
|
||||||
|
[Migration("20260916111422_AddBozzaScheda")]
|
||||||
|
partial class AddBozzaScheda
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "10.0.7");
|
||||||
|
|
||||||
|
modelBuilder.Entity("SteUp.Shared.Core.Entities.Ispezione", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("CodMdep")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Data")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Rilevatore")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ActivityId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Stato")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("CodMdep", "Data", "Rilevatore");
|
||||||
|
|
||||||
|
b.ToTable("Ispezioni");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SteUp.Shared.Core.Entities.Scheda", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ActivityId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ActivityTypeId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("Bozza")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("CodJfas")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("CodMdep")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Data")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DescrizioneReparto")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ImageNames")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Note")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Responsabile")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Rilevatore")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Scadenza")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CodMdep", "Data", "Rilevatore");
|
||||||
|
|
||||||
|
b.ToTable("Schede");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SteUp.Shared.Core.Entities.SchedaArticolo", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Barcode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Descrizione")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("SchedaId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SchedaId", "Barcode")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("SchedaArticoli");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SteUp.Shared.Core.Entities.Scheda", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SteUp.Shared.Core.Entities.Ispezione", "Ispezione")
|
||||||
|
.WithMany("Schede")
|
||||||
|
.HasForeignKey("CodMdep", "Data", "Rilevatore")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Ispezione");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SteUp.Shared.Core.Entities.SchedaArticolo", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SteUp.Shared.Core.Entities.Scheda", "Scheda")
|
||||||
|
.WithMany("Articoli")
|
||||||
|
.HasForeignKey("SchedaId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Scheda");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SteUp.Shared.Core.Entities.Ispezione", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Schede");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SteUp.Shared.Core.Entities.Scheda", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Articoli");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace SteUp.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddBozzaScheda : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "Bozza",
|
||||||
|
table: "Schede",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Bozza",
|
||||||
|
table: "Schede");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ namespace SteUp.Data.Migrations
|
|||||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.3");
|
modelBuilder.HasAnnotation("ProductVersion", "10.0.7");
|
||||||
|
|
||||||
modelBuilder.Entity("SteUp.Shared.Core.Entities.Ispezione", b =>
|
modelBuilder.Entity("SteUp.Shared.Core.Entities.Ispezione", b =>
|
||||||
{
|
{
|
||||||
@@ -51,6 +51,9 @@ namespace SteUp.Data.Migrations
|
|||||||
b.Property<string>("ActivityTypeId")
|
b.Property<string>("ActivityTypeId")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("Bozza")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<string>("CodJfas")
|
b.Property<string>("CodJfas")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ using SteUp.Shared.Core.Interface.System.Network;
|
|||||||
using SteUp.Shared.Core.Messages.Ispezione;
|
using SteUp.Shared.Core.Messages.Ispezione;
|
||||||
using SteUp.Shared.Core.Messages.Scanner;
|
using SteUp.Shared.Core.Messages.Scanner;
|
||||||
using SteUp.Shared.Core.Messages.Scheda;
|
using SteUp.Shared.Core.Messages.Scheda;
|
||||||
|
using SteUp.Shared.Core.Messages.System;
|
||||||
using SteUp.Shared.Core.Services;
|
using SteUp.Shared.Core.Services;
|
||||||
|
|
||||||
namespace SteUp.Maui.Core;
|
namespace SteUp.Maui.Core;
|
||||||
@@ -65,6 +66,7 @@ public static class CoreModule
|
|||||||
builder.Services.AddSingleton<NewSchedaService>();
|
builder.Services.AddSingleton<NewSchedaService>();
|
||||||
builder.Services.AddSingleton<OnScannerService>();
|
builder.Services.AddSingleton<OnScannerService>();
|
||||||
builder.Services.AddSingleton<CompleteInspectionService>();
|
builder.Services.AddSingleton<CompleteInspectionService>();
|
||||||
|
builder.Services.AddSingleton<BackButtonService>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RegisterDbServices()
|
public void RegisterDbServices()
|
||||||
|
|||||||
@@ -164,7 +164,14 @@ public partial class HoneywellScannerService
|
|||||||
: Java.Lang.Object, BarcodeReader.IBarcodeListener
|
: Java.Lang.Object, BarcodeReader.IBarcodeListener
|
||||||
{
|
{
|
||||||
public void OnBarcodeEvent(BarcodeReadEvent? e) => service.DispatchEvent(e!);
|
public void OnBarcodeEvent(BarcodeReadEvent? e) => service.DispatchEvent(e!);
|
||||||
public void OnFailureEvent(BarcodeFailureEvent? e) =>
|
|
||||||
service.OnScanFailed?.Invoke(new Exception($"Scan failure: {e}"));
|
// Honeywell emette BarcodeFailureEvent anche solo premendo e rilasciando il
|
||||||
|
// trigger senza inquadrare nulla: e' la normale mancata lettura, non un guasto.
|
||||||
|
// Trattarlo come eccezione faceva comparire un dialog bloccante col toString()
|
||||||
|
// Java a ogni pressione a vuoto. Non c'e' nulla da comunicare: chi ha premuto
|
||||||
|
// il trigger sa gia' di non aver letto niente e sta ripuntando lo scanner.
|
||||||
|
public void OnFailureEvent(BarcodeFailureEvent? e)
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,61 @@
|
|||||||
using Android.App;
|
using Android.App;
|
||||||
using Android.Content.PM;
|
using Android.Content.PM;
|
||||||
|
using Android.OS;
|
||||||
|
using Android.Views;
|
||||||
|
using AndroidX.Core.View;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using SteUp.Shared.Core.Messages.System;
|
||||||
|
|
||||||
namespace SteUp.Maui
|
namespace SteUp.Maui
|
||||||
{
|
{
|
||||||
[Activity(
|
[Activity(
|
||||||
Theme = "@style/Maui.SplashTheme",
|
Theme = "@style/Maui.SplashTheme",
|
||||||
MainLauncher = true,
|
MainLauncher = true,
|
||||||
|
WindowSoftInputMode = SoftInput.AdjustResize | SoftInput.StateHidden,
|
||||||
ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode |
|
ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode |
|
||||||
ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
|
ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
|
||||||
public class MainActivity : MauiAppCompatActivity
|
public class MainActivity : MauiAppCompatActivity
|
||||||
{
|
{
|
||||||
|
protected override void OnCreate(Bundle? savedInstanceState)
|
||||||
|
{
|
||||||
|
base.OnCreate(savedInstanceState);
|
||||||
|
|
||||||
|
// Da Android 15 (API 35) il edge-to-edge e' forzato e adjustResize non ridimensiona
|
||||||
|
// piu' la finestra: la tastiera si limita a coprire la WebView. Riduciamo noi il
|
||||||
|
// content view dell'altezza dell'IME cosi' il layout Blazor (100vh) si ricalcola.
|
||||||
|
if (FindViewById(Android.Resource.Id.Content) is { } content)
|
||||||
|
ViewCompat.SetOnApplyWindowInsetsListener(content, new ImeInsetsListener());
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnBackPressed()
|
||||||
|
{
|
||||||
|
// Se la UI Blazor ha un form aperto se ne occupa lei (propone il salvataggio
|
||||||
|
// come bozza); altrimenti comportamento standard.
|
||||||
|
var backButton = IPlatformApplication.Current?.Services.GetService<BackButtonService>();
|
||||||
|
if (backButton?.TryHandleBackPressed() == true) return;
|
||||||
|
|
||||||
|
base.OnBackPressed();
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class ImeInsetsListener : Java.Lang.Object, IOnApplyWindowInsetsListener
|
||||||
|
{
|
||||||
|
public WindowInsetsCompat OnApplyWindowInsets(Android.Views.View? v, WindowInsetsCompat? insets)
|
||||||
|
{
|
||||||
|
if (v is null || insets is null)
|
||||||
|
return insets!;
|
||||||
|
|
||||||
|
var ime = insets.GetInsets(WindowInsetsCompat.Type.Ime()).Bottom;
|
||||||
|
var systemBars = insets.GetInsets(WindowInsetsCompat.Type.SystemBars()).Bottom;
|
||||||
|
|
||||||
|
// La barra di navigazione e' gia' gestita da SafeAreaEdges in MainPage:
|
||||||
|
// teniamo solo la parte di tastiera che la eccede, per non sommare due volte.
|
||||||
|
var bottom = System.Math.Max(0, ime - systemBars);
|
||||||
|
|
||||||
|
if (v.PaddingBottom != bottom)
|
||||||
|
v.SetPadding(v.PaddingLeft, v.PaddingTop, v.PaddingRight, bottom);
|
||||||
|
|
||||||
|
return insets;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@
|
|||||||
<TitleContent>
|
<TitleContent>
|
||||||
@{
|
@{
|
||||||
var daInviare = group.Value.CountDaInviare();
|
var daInviare = group.Value.CountDaInviare();
|
||||||
|
var daCompletare = group.Value.CountBozze();
|
||||||
}
|
}
|
||||||
<div class="header-scheda-group">
|
<div class="header-scheda-group">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
@@ -54,6 +55,13 @@
|
|||||||
@($"{daInviare} da inviare")
|
@($"{daInviare} da inviare")
|
||||||
</MudChip>
|
</MudChip>
|
||||||
}
|
}
|
||||||
|
@if (daCompletare > 0)
|
||||||
|
{
|
||||||
|
<MudChip T="string" Size="Size.Small" Variant="Variant.Filled"
|
||||||
|
Color="Color.Warning" Icon="@Icons.Material.Rounded.EditNote">
|
||||||
|
@($"{daCompletare} da completare")
|
||||||
|
</MudChip>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
@if (SteupDataService.InspectionPageState.Ispezione.Stato != StatusEnum.Completata)
|
@if (SteupDataService.InspectionPageState.Ispezione.Stato != StatusEnum.Completata)
|
||||||
{
|
{
|
||||||
@@ -156,7 +164,25 @@
|
|||||||
|
|
||||||
var ispezione = SteupDataService.InspectionPageState.Ispezione;
|
var ispezione = SteupDataService.InspectionPageState.Ispezione;
|
||||||
|
|
||||||
var daInviare = ispezione.Schede.Where(x => x.ActivityId == null).ToList();
|
// Le bozze non sono inviabili: l'ispezione non si chiude finché ci sono.
|
||||||
|
var bozze = SchedeList.CountBozze();
|
||||||
|
if (bozze > 0)
|
||||||
|
{
|
||||||
|
await InvokeAsync(() =>
|
||||||
|
{
|
||||||
|
VisibleOverlay = false;
|
||||||
|
StateHasChanged();
|
||||||
|
});
|
||||||
|
|
||||||
|
await Dialog.ShowWarning(
|
||||||
|
bozze == 1
|
||||||
|
? "C'è 1 scheda da completare. Completala o eliminala prima di concludere l'ispezione."
|
||||||
|
: $"Ci sono {bozze} schede da completare. Completale o eliminale prima di concludere l'ispezione."
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var daInviare = ispezione.Schede.Where(x => x.ActivityId == null && !x.Bozza).ToList();
|
||||||
var totale = daInviare.Count;
|
var totale = daInviare.Count;
|
||||||
var indice = 0;
|
var indice = 0;
|
||||||
var fallite = 0;
|
var fallite = 0;
|
||||||
@@ -341,7 +367,16 @@
|
|||||||
{
|
{
|
||||||
var ispezione = SteupDataService.InspectionPageState.Ispezione;
|
var ispezione = SteupDataService.InspectionPageState.Ispezione;
|
||||||
|
|
||||||
var saveRequest = SchedeGrouped[jtbFasi].ConvertAll(x =>
|
// Le bozze restano sul dispositivo: si esporta solo ciò che è completo.
|
||||||
|
var daEsportare = SchedeGrouped[jtbFasi].FindAll(x => !x.Bozza);
|
||||||
|
|
||||||
|
if (daEsportare.Count == 0)
|
||||||
|
{
|
||||||
|
Snackbar.Add("Nessuna scheda completa da esportare in questo reparto", Severity.Info);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var saveRequest = daEsportare.ConvertAll(x =>
|
||||||
{
|
{
|
||||||
return new SaveRequestDto
|
return new SaveRequestDto
|
||||||
{
|
{
|
||||||
@@ -375,7 +410,7 @@
|
|||||||
|
|
||||||
if (!apiResponse.ActivityIdSchedaList.IsNullOrEmpty())
|
if (!apiResponse.ActivityIdSchedaList.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
foreach (var scheda in SchedeGrouped[jtbFasi])
|
foreach (var scheda in daEsportare)
|
||||||
{
|
{
|
||||||
var activityId = apiResponse.ActivityIdSchedaList!.Find(x =>
|
var activityId = apiResponse.ActivityIdSchedaList!.Find(x =>
|
||||||
x.LocalId != null && x.LocalId == scheda.Id
|
x.LocalId != null && x.LocalId == scheda.Id
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
<MudMessageBox @ref="_confirmSave" Title="Attenzione!" CancelText="Non salvare">
|
@* NoText -> false (salva bozza), YesText -> true (salva), chiusura senza scelta -> null.
|
||||||
|
Niente CancelText: da qui non si esce buttando via il lavoro. *@
|
||||||
|
<MudMessageBox @ref="_confirmSave" Title="Lavoro non salvato" NoText="Salva come bozza">
|
||||||
<MessageContent>
|
<MessageContent>
|
||||||
Sono state apportate delle modifiche. Vuoi salvarle prima di continuare?
|
Hai delle modifiche non salvate. Salva la scheda, oppure tienila come bozza
|
||||||
|
da completare più tardi: in ogni caso non perdi nulla.
|
||||||
</MessageContent>
|
</MessageContent>
|
||||||
<YesButton>
|
<YesButton>
|
||||||
<MudButton Size="Size.Small" Variant="Variant.Filled" Color="Color.Primary">
|
<MudButton Size="Size.Small" Variant="Variant.Filled" Color="Color.Primary">
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
@using SteUp.Shared.Core.Interface.LocalDb
|
@using SteUp.Shared.Core.Interface.LocalDb
|
||||||
@using SteUp.Shared.Core.Interface.System
|
@using SteUp.Shared.Core.Interface.System
|
||||||
@using SteUp.Shared.Core.Messages.Scanner
|
@using SteUp.Shared.Core.Messages.Scanner
|
||||||
|
@using SteUp.Shared.Core.Messages.System
|
||||||
|
@inject BackButtonService BackButton
|
||||||
@inject INetworkService NetworkService
|
@inject INetworkService NetworkService
|
||||||
@inject IDialogService Dialog
|
@inject IDialogService Dialog
|
||||||
@inject IIntegryApiService IntegryApiService
|
@inject IIntegryApiService IntegryApiService
|
||||||
@@ -20,6 +22,7 @@
|
|||||||
@inject OnScannerService OnScannerService
|
@inject OnScannerService OnScannerService
|
||||||
@inject IAttachedService AttachedService
|
@inject IAttachedService AttachedService
|
||||||
@inject ILogger<ModalFormScheda> Logger
|
@inject ILogger<ModalFormScheda> Logger
|
||||||
|
@implements IDisposable
|
||||||
|
|
||||||
<MudDialog Class="customDialog-form">
|
<MudDialog Class="customDialog-form">
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
@@ -230,8 +233,41 @@
|
|||||||
private int _noteFieldKey;
|
private int _noteFieldKey;
|
||||||
private bool IsDirty { get; set; }
|
private bool IsDirty { get; set; }
|
||||||
private Scheda _originalScheda = null!;
|
private Scheda _originalScheda = null!;
|
||||||
|
// Clone() e' un MemberwiseClone, quindi _originalScheda condivide la stessa lista
|
||||||
|
// Articoli: per accorgerci degli articoli aggiunti/rimossi serve uno snapshot a parte.
|
||||||
|
private List<string> _originalBarcodes = [];
|
||||||
|
// Gli allegati vivono in AttachedList finche' non si salva: li tracciamo con un flag.
|
||||||
|
private bool _attachmentsDirty;
|
||||||
private List<AttachedDto>? AttachedList { get; set; }
|
private List<AttachedDto>? AttachedList { get; set; }
|
||||||
|
|
||||||
|
private IDisposable? _backRegistration;
|
||||||
|
// Il tasto indietro puo' arrivare mentre il prompt di uscita e' gia' a video.
|
||||||
|
private bool _closing;
|
||||||
|
// Dialog aperti sopra al form (articoli, suggerimenti, errori).
|
||||||
|
private int _childDialogs;
|
||||||
|
|
||||||
|
private Task HandleBackPressed()
|
||||||
|
{
|
||||||
|
// Con un dialog figlio a video il back non deve agire sul form sottostante:
|
||||||
|
// meglio ignorarlo che far comparire il prompt di uscita sotto un altro dialog.
|
||||||
|
if (_childDialogs > 0) return Task.CompletedTask;
|
||||||
|
|
||||||
|
return Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<T> WithChildDialog<T>(Func<Task<T>> open)
|
||||||
|
{
|
||||||
|
_childDialogs++;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await open();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_childDialogs--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected override void OnInitialized()
|
protected override void OnInitialized()
|
||||||
{
|
{
|
||||||
Snackbar.Configuration.PositionClass = Defaults.Classes.Position.TopCenter;
|
Snackbar.Configuration.PositionClass = Defaults.Classes.Position.TopCenter;
|
||||||
@@ -240,8 +276,13 @@
|
|||||||
OnScannerService.OnErrorScan += OnErrorScan;
|
OnScannerService.OnErrorScan += OnErrorScan;
|
||||||
|
|
||||||
_originalScheda = Scheda.Clone();
|
_originalScheda = Scheda.Clone();
|
||||||
|
_originalBarcodes = Scheda.Articoli.ConvertAll(x => x.Barcode);
|
||||||
Snackbar.Configuration.PositionClass = Defaults.Classes.Position.TopCenter;
|
Snackbar.Configuration.PositionClass = Defaults.Classes.Position.TopCenter;
|
||||||
|
|
||||||
|
// Il tasto indietro del dispositivo deve passare da qui, non mandare l'app
|
||||||
|
// in background lasciando il lavoro non salvato solo in memoria.
|
||||||
|
_backRegistration = BackButton.Register(() => InvokeAsync(HandleBackPressed));
|
||||||
|
|
||||||
LoadAttached();
|
LoadAttached();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,7 +319,10 @@
|
|||||||
{
|
{
|
||||||
VisibleOverlay = true;
|
VisibleOverlay = true;
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
|
|
||||||
|
// Salvataggio completo: la scheda non è più una bozza.
|
||||||
|
Scheda.Bozza = false;
|
||||||
|
|
||||||
SaveSchedaResponseDto? apiResponse = null;
|
SaveSchedaResponseDto? apiResponse = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -363,7 +407,7 @@
|
|||||||
await IspezioniService.AddSchedaAsync(CodMdep, Data, UserSession.User.Username, Scheda);
|
await IspezioniService.AddSchedaAsync(CodMdep, Data, UserSession.User.Username, Scheda);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task Update(SaveSchedaResponseDto? apiResponse)
|
private async Task Update(SaveSchedaResponseDto? apiResponse, bool preserveActivityId = false)
|
||||||
{
|
{
|
||||||
if (!AttachedList.IsNullOrEmpty())
|
if (!AttachedList.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
@@ -393,7 +437,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Scheda.ActivityId = apiResponse?.ActivityIdScheda;
|
// Salvando una bozza non tocchiamo l'ActivityId: se la scheda era già sul server,
|
||||||
|
// al completamento va aggiornata, non ricreata.
|
||||||
|
if (!preserveActivityId) Scheda.ActivityId = apiResponse?.ActivityIdScheda;
|
||||||
|
|
||||||
await IspezioniService.UpdateSchedaAsync(Scheda);
|
await IspezioniService.UpdateSchedaAsync(Scheda);
|
||||||
|
|
||||||
if (apiResponse != null)
|
if (apiResponse != null)
|
||||||
@@ -434,11 +481,21 @@
|
|||||||
|
|
||||||
private async Task Cancel()
|
private async Task Cancel()
|
||||||
{
|
{
|
||||||
if (await CheckSavePreAction())
|
if (_closing) return;
|
||||||
|
_closing = true;
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
await FileManager.CleanTempStorageAsync();
|
if (await CheckSavePreAction())
|
||||||
DisposeMessage();
|
{
|
||||||
MudDialog.Cancel();
|
await FileManager.CleanTempStorageAsync();
|
||||||
|
DisposeMessage();
|
||||||
|
MudDialog.Cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_closing = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -456,14 +513,29 @@
|
|||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RecalcDirty(bool forceTrue = false)
|
private void RecalcDirty()
|
||||||
{
|
{
|
||||||
IsDirty = forceTrue || !ValueComparer.AreEqual(Scheda, _originalScheda);
|
IsDirty = _attachmentsDirty
|
||||||
|
|| !ValueComparer.AreEqual(Scheda, _originalScheda)
|
||||||
|
|| !BarcodesUnchanged();
|
||||||
|
|
||||||
if (IsDirty) LabelSave = !IsNew ? "Aggiorna" : "Salva";
|
if (IsDirty) LabelSave = !IsNew ? "Aggiorna" : "Salva";
|
||||||
else LabelSave = null;
|
else LabelSave = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool BarcodesUnchanged()
|
||||||
|
{
|
||||||
|
if (Scheda.Articoli.Count != _originalBarcodes.Count) return false;
|
||||||
|
|
||||||
|
for (var i = 0; i < _originalBarcodes.Count; i++)
|
||||||
|
{
|
||||||
|
if (!Scheda.Articoli[i].Barcode.Equals(_originalBarcodes[i], StringComparison.Ordinal))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private static class ValueComparer
|
private static class ValueComparer
|
||||||
{
|
{
|
||||||
public static bool AreEqual(Scheda? a, Scheda? b)
|
public static bool AreEqual(Scheda? a, Scheda? b)
|
||||||
@@ -480,17 +552,25 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ritorna true se il form può chiudersi scartando (nessuna modifica in sospeso).
|
||||||
|
/// Con del lavoro in corso non si perde mai nulla: o si salva per intero, o si
|
||||||
|
/// salva come bozza da riprendere.
|
||||||
|
/// </summary>
|
||||||
private async Task<bool> CheckSavePreAction()
|
private async Task<bool> CheckSavePreAction()
|
||||||
{
|
{
|
||||||
if (!IsDirty) return true;
|
if (!IsDirty) return true;
|
||||||
|
|
||||||
var resul = await _confirmUpdateMessage.ShowAsync();
|
var result = await WithChildDialog(() => _confirmUpdateMessage.ShowAsync());
|
||||||
if (resul is not true) return true;
|
|
||||||
|
// Prompt chiuso senza scegliere (tap fuori / indietro): si resta sulla scheda.
|
||||||
|
if (result is null) return false;
|
||||||
|
|
||||||
VisibleOverlay = true;
|
VisibleOverlay = true;
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
|
|
||||||
await Submit();
|
if (result is true) await Submit();
|
||||||
|
else await SaveDraft();
|
||||||
|
|
||||||
VisibleOverlay = false;
|
VisibleOverlay = false;
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
@@ -498,10 +578,48 @@
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Salva il lavoro in corso solo sul dispositivo, marcato come da completare.
|
||||||
|
/// Niente chiamate al server e niente validazione: una bozza può essere incompleta.
|
||||||
|
/// </summary>
|
||||||
|
private async Task SaveDraft()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Scheda.Bozza = true;
|
||||||
|
|
||||||
|
if (IsNew) await NewSave(null);
|
||||||
|
else await Update(null, preserveActivityId: true);
|
||||||
|
|
||||||
|
await FileManager.CleanTempStorageAsync();
|
||||||
|
|
||||||
|
Snackbar.Clear();
|
||||||
|
Snackbar.Add("Scheda salvata come bozza, la ritrovi nell'ispezione", Severity.Success);
|
||||||
|
|
||||||
|
DisposeMessage();
|
||||||
|
MudDialog.Close(Scheda);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Scheda.Bozza = false;
|
||||||
|
OnError(e, e.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task Submit()
|
private async Task Submit()
|
||||||
{
|
{
|
||||||
await _form.Validate();
|
await _form.Validate();
|
||||||
if (_form.IsValid) await Save();
|
|
||||||
|
if (_form.IsValid)
|
||||||
|
{
|
||||||
|
await Save();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Il form resta aperto sugli errori: senza un avviso il tocco su "Salva"
|
||||||
|
// sembra non fare nulla se i campi mancanti sono fuori schermo.
|
||||||
|
Snackbar.Clear();
|
||||||
|
Snackbar.Add("Compila i campi obbligatori, oppure esci salvando come bozza", Severity.Warning);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@@ -526,7 +644,8 @@
|
|||||||
|
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
RecalcDirty(true);
|
_attachmentsDirty = true;
|
||||||
|
RecalcDirty();
|
||||||
|
|
||||||
// Processa in background e aggiorna UI man mano (o a blocchi)
|
// Processa in background e aggiorna UI man mano (o a blocchi)
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
@@ -599,7 +718,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var modal = await Dialog.OpenSelectArt(articoli);
|
var modal = await WithChildDialog(() => Dialog.OpenSelectArt(articoli));
|
||||||
|
|
||||||
await InvokeAsync(() =>
|
await InvokeAsync(() =>
|
||||||
{
|
{
|
||||||
@@ -618,7 +737,11 @@
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(() =>
|
||||||
|
{
|
||||||
|
RecalcDirty();
|
||||||
|
StateHasChanged();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -631,7 +754,8 @@
|
|||||||
AttachedList[index].ToRemove = true;
|
AttachedList[index].ToRemove = true;
|
||||||
|
|
||||||
AttachedList.RemoveAt(index);
|
AttachedList.RemoveAt(index);
|
||||||
RecalcDirty(true);
|
_attachmentsDirty = true;
|
||||||
|
RecalcDirty();
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -652,7 +776,7 @@
|
|||||||
{
|
{
|
||||||
var activityDescriptions = await IntegryApiService.SuggestActivityDescription(Scheda.ActivityTypeId);
|
var activityDescriptions = await IntegryApiService.SuggestActivityDescription(Scheda.ActivityTypeId);
|
||||||
|
|
||||||
var modal = await Dialog.OpenSuggestActivityDescription(activityDescriptions);
|
var modal = await WithChildDialog(() => Dialog.OpenSuggestActivityDescription(activityDescriptions));
|
||||||
|
|
||||||
if (modal is { Canceled: false, Data: string descrizione } && !string.IsNullOrWhiteSpace(descrizione))
|
if (modal is { Canceled: false, Data: string descrizione } && !string.IsNullOrWhiteSpace(descrizione))
|
||||||
{
|
{
|
||||||
@@ -701,14 +825,13 @@
|
|||||||
|
|
||||||
if (art != null)
|
if (art != null)
|
||||||
{
|
{
|
||||||
RecalcDirty(true);
|
|
||||||
|
|
||||||
Scheda.Articoli.Insert(0, new SchedaArticolo
|
Scheda.Articoli.Insert(0, new SchedaArticolo
|
||||||
{
|
{
|
||||||
Barcode = art.Barcode,
|
Barcode = art.Barcode,
|
||||||
Descrizione = art.Descrizione
|
Descrizione = art.Descrizione
|
||||||
});
|
});
|
||||||
|
|
||||||
|
RecalcDirty();
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -748,8 +871,8 @@
|
|||||||
var index = Scheda.Articoli.FindIndex(x => x.Barcode.Equals(barcode));
|
var index = Scheda.Articoli.FindIndex(x => x.Barcode.Equals(barcode));
|
||||||
if (index < 0) return;
|
if (index < 0) return;
|
||||||
|
|
||||||
RecalcDirty(true);
|
|
||||||
Scheda.Articoli.RemoveAt(index);
|
Scheda.Articoli.RemoveAt(index);
|
||||||
|
RecalcDirty();
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -757,6 +880,16 @@
|
|||||||
{
|
{
|
||||||
OnScannerService.OnNewScanSuccessful -= HandleNewScanSuccessful;
|
OnScannerService.OnNewScanSuccessful -= HandleNewScanSuccessful;
|
||||||
OnScannerService.OnErrorScan -= OnErrorScan;
|
OnScannerService.OnErrorScan -= OnErrorScan;
|
||||||
|
|
||||||
|
_backRegistration?.Dispose();
|
||||||
|
_backRegistration = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rete di sicurezza: se il dialog viene chiuso per altre vie, il tasto indietro
|
||||||
|
// non deve restare agganciato a un form che non esiste più.
|
||||||
|
void IDisposable.Dispose()
|
||||||
|
{
|
||||||
|
DisposeMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -26,6 +26,8 @@ public class BarcodeManager(
|
|||||||
|
|
||||||
if (IsEanPeso(dto))
|
if (IsEanPeso(dto))
|
||||||
stringValue = DecodeEanPeso(stringValue);
|
stringValue = DecodeEanPeso(stringValue);
|
||||||
|
else if (IsPlu(dto))
|
||||||
|
stringValue = DecodePlu(stringValue);
|
||||||
|
|
||||||
messenger.Send(new NewScannerMessage(stringValue));
|
messenger.Send(new NewScannerMessage(stringValue));
|
||||||
},
|
},
|
||||||
@@ -39,14 +41,29 @@ public class BarcodeManager(
|
|||||||
(IsEtichetta128(barcodeScan) || IsEan13(barcodeScan)) && barcodeScan.StringValue is { Length: 13 } &&
|
(IsEtichetta128(barcodeScan) || IsEan13(barcodeScan)) && barcodeScan.StringValue is { Length: 13 } &&
|
||||||
barcodeScan.StringValue.StartsWith('2');
|
barcodeScan.StringValue.StartsWith('2');
|
||||||
|
|
||||||
|
private static bool IsPlu(BarcodeScanDto barcodeScan) =>
|
||||||
|
IsCode128(barcodeScan) && barcodeScan.StringValue is { Length: 7 } && barcodeScan.StringValue.StartsWith('2');
|
||||||
|
|
||||||
private static bool IsEtichetta128(BarcodeScanDto barcodeScan) =>
|
private static bool IsEtichetta128(BarcodeScanDto barcodeScan) =>
|
||||||
barcodeScan.Type is BarcodeType.CODE128 or BarcodeType.EAN128;
|
barcodeScan.Type is BarcodeType.CODE128 or BarcodeType.EAN128;
|
||||||
|
|
||||||
|
private static bool IsCode128(BarcodeScanDto barcodeScan) =>
|
||||||
|
barcodeScan.Type == BarcodeType.CODE128;
|
||||||
|
|
||||||
private static bool IsEan13(BarcodeScanDto barcodeScan) =>
|
private static bool IsEan13(BarcodeScanDto barcodeScan) =>
|
||||||
barcodeScan.Type == BarcodeType.EAN13;
|
barcodeScan.Type == BarcodeType.EAN13;
|
||||||
|
|
||||||
private static string DecodeEanPeso(string? barcode)
|
private static string DecodeEanPeso(string? barcode)
|
||||||
{
|
{
|
||||||
return barcode is not { Length: 13 } ? throw new Exception("Errore durante il parse del barcode (" + barcode + ")") : barcode.Substring(1, 6);
|
return barcode is not { Length: 13 }
|
||||||
|
? throw new Exception("Errore durante il parse del barcode (" + barcode + ")")
|
||||||
|
: barcode.Substring(1, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string DecodePlu(string? barcode)
|
||||||
|
{
|
||||||
|
return barcode is not { Length: 7 }
|
||||||
|
? throw new Exception("Errore durante il parse del barcode (" + barcode + ")")
|
||||||
|
: barcode.Substring(1, 6);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -20,6 +20,12 @@ public class Scheda : EntityBase<Scheda>
|
|||||||
|
|
||||||
public List<SchedaArticolo> Articoli { get; set; } = [];
|
public List<SchedaArticolo> Articoli { get; set; } = [];
|
||||||
public List<string>? ImageNames { get; set; }
|
public List<string>? ImageNames { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Scheda salvata in locale ma non ancora completata (es. uscita dal form con lavoro in corso).
|
||||||
|
/// Non viene mai inviata al server finché il rilevatore non la conclude.
|
||||||
|
/// </summary>
|
||||||
|
public bool Bozza { get; set; }
|
||||||
|
|
||||||
public string? DescrizioneReparto { get; set; }
|
public string? DescrizioneReparto { get; set; }
|
||||||
public string? ActivityTypeId { get; set; }
|
public string? ActivityTypeId { get; set; }
|
||||||
|
|||||||
@@ -14,19 +14,27 @@ public enum SyncState
|
|||||||
DaInviare = 0,
|
DaInviare = 0,
|
||||||
|
|
||||||
/// Inviata al server (ha un ActivityId).
|
/// Inviata al server (ha un ActivityId).
|
||||||
Inviato = 1
|
Inviato = 1,
|
||||||
|
|
||||||
|
/// Salvata in locale ma non completata: da riprendere, non inviabile.
|
||||||
|
Bozza = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class SchedaSyncHelper
|
public static class SchedaSyncHelper
|
||||||
{
|
{
|
||||||
public static SyncState GetSyncState(this Scheda scheda) =>
|
public static SyncState GetSyncState(this Scheda scheda) =>
|
||||||
string.IsNullOrEmpty(scheda.ActivityId) ? SyncState.DaInviare : SyncState.Inviato;
|
scheda.Bozza
|
||||||
|
? SyncState.Bozza
|
||||||
|
: string.IsNullOrEmpty(scheda.ActivityId)
|
||||||
|
? SyncState.DaInviare
|
||||||
|
: SyncState.Inviato;
|
||||||
|
|
||||||
public static string ToLabel(this SyncState state) =>
|
public static string ToLabel(this SyncState state) =>
|
||||||
state switch
|
state switch
|
||||||
{
|
{
|
||||||
SyncState.DaInviare => "Da inviare",
|
SyncState.DaInviare => "Da inviare",
|
||||||
SyncState.Inviato => "Inviato",
|
SyncState.Inviato => "Inviato",
|
||||||
|
SyncState.Bozza => "Da completare",
|
||||||
_ => string.Empty
|
_ => string.Empty
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -39,6 +47,7 @@ public static class SchedaSyncHelper
|
|||||||
{
|
{
|
||||||
SyncState.DaInviare => Color.Primary,
|
SyncState.DaInviare => Color.Primary,
|
||||||
SyncState.Inviato => Color.Success,
|
SyncState.Inviato => Color.Success,
|
||||||
|
SyncState.Bozza => Color.Warning,
|
||||||
_ => Color.Default
|
_ => Color.Default
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -47,10 +56,16 @@ public static class SchedaSyncHelper
|
|||||||
{
|
{
|
||||||
SyncState.DaInviare => Icons.Material.Rounded.PhonelinkOff,
|
SyncState.DaInviare => Icons.Material.Rounded.PhonelinkOff,
|
||||||
SyncState.Inviato => Icons.Material.Rounded.CloudDone,
|
SyncState.Inviato => Icons.Material.Rounded.CloudDone,
|
||||||
|
SyncState.Bozza => Icons.Material.Rounded.EditNote,
|
||||||
_ => string.Empty
|
_ => string.Empty
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Numero di schede non ancora inviate in una lista.
|
/// Numero di schede non ancora inviate in una lista. Le bozze non contano:
|
||||||
|
/// non sono inviabili finché non vengono completate.
|
||||||
public static int CountDaInviare(this IEnumerable<Scheda> schede) =>
|
public static int CountDaInviare(this IEnumerable<Scheda> schede) =>
|
||||||
schede.Count(s => s.GetSyncState() == SyncState.DaInviare);
|
schede.Count(s => s.GetSyncState() == SyncState.DaInviare);
|
||||||
|
|
||||||
|
/// Numero di schede lasciate a metà, da riprendere.
|
||||||
|
public static int CountBozze(this IEnumerable<Scheda> schede) =>
|
||||||
|
schede.Count(s => s.GetSyncState() == SyncState.Bozza);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
namespace SteUp.Shared.Core.Messages.System;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ponte fra il tasto indietro del dispositivo (host nativo) e la UI Blazor.
|
||||||
|
/// Il form scheda si registra mentre è aperto così il back non manda l'app in
|
||||||
|
/// background buttando via il lavoro non salvato: passa invece dallo stesso
|
||||||
|
/// percorso del pulsante "Annulla", che propone il salvataggio come bozza.
|
||||||
|
/// </summary>
|
||||||
|
public class BackButtonService
|
||||||
|
{
|
||||||
|
private Func<Task>? _handler;
|
||||||
|
|
||||||
|
/// Registra il gestore corrente. Ritorna l'azione di deregistrazione.
|
||||||
|
public IDisposable Register(Func<Task> handler)
|
||||||
|
{
|
||||||
|
_handler = handler;
|
||||||
|
return new Registration(this, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invocato dall'host nativo. Ritorna true se la UI Blazor ha preso in carico
|
||||||
|
/// il tasto indietro: in quel caso l'host non deve fare altro.
|
||||||
|
/// </summary>
|
||||||
|
public bool TryHandleBackPressed()
|
||||||
|
{
|
||||||
|
var handler = _handler;
|
||||||
|
if (handler is null) return false;
|
||||||
|
|
||||||
|
// Fire-and-forget: il gestore rientra nel dispatcher Blazor da solo.
|
||||||
|
_ = handler();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class Registration(BackButtonService owner, Func<Task> handler) : IDisposable
|
||||||
|
{
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
// Non azzerare se nel frattempo si è registrato qualcun altro.
|
||||||
|
if (ReferenceEquals(owner._handler, handler)) owner._handler = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -95,3 +95,38 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
}, 600);
|
}, 600);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
||||||
|
// Tastiera: porta il campo attivo dentro l'area visibile.
|
||||||
|
// Il layout e' a 100vh con overflow:hidden, quindi quando l'host riduce la
|
||||||
|
// WebView per la tastiera il browser non scrolla da solo sul campo col focus.
|
||||||
|
(function () {
|
||||||
|
const SELECTOR = 'input, textarea, [contenteditable="true"]';
|
||||||
|
let pending = null;
|
||||||
|
|
||||||
|
function scrollFocusedIntoView() {
|
||||||
|
const el = document.activeElement;
|
||||||
|
if (!el || !el.matches || !el.matches(SELECTOR)) return;
|
||||||
|
el.scrollIntoView({block: 'center', behavior: 'smooth'});
|
||||||
|
}
|
||||||
|
|
||||||
|
function schedule() {
|
||||||
|
clearTimeout(pending);
|
||||||
|
// attende la fine dell'animazione di apertura della tastiera e del relayout
|
||||||
|
pending = setTimeout(scrollFocusedIntoView, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('focusin', function (event) {
|
||||||
|
if (event.target && event.target.matches && event.target.matches(SELECTOR)) {
|
||||||
|
schedule();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Il resize arriva quando la tastiera si apre/chiude o cambia altezza
|
||||||
|
// (es. passaggio a tastiera emoji o suggerimenti).
|
||||||
|
if (window.visualViewport) {
|
||||||
|
window.visualViewport.addEventListener('resize', schedule);
|
||||||
|
} else {
|
||||||
|
window.addEventListener('resize', schedule);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|||||||
Reference in New Issue
Block a user