Togli il + dalla barra e porta le azioni nella testata

Il "+" stava incastrato a destra nella barra di navigazione, che per il resto
contiene destinazioni. Cambiava significato da una schermata all'altra senza
mai dichiararlo, e su Ispezioni apriva un menu per far scegliere fra una cosa
sola. Il documento di stile esclude esplicitamente il FAB di serie, e la barra
torna a contenere solo le due destinazioni.

Su Ispezioni l'azione e' un bottone con il suo nome nella testata, e porta
dritta alla scelta del punto vendita senza passaggi intermedi. Lo stato vuoto
offre lo stesso comando invece di spiegare dove trovarlo.

Su Ispezione le azioni sono tre e con pesi diversi: restano in un bottom sheet,
aperto dalla testata. Eliminare un'ispezione ora chiede conferma — prima un
tocco cancellava ispezione e schede senza appello.

Le azioni vivono nella pagina che le esegue, quindi i messaggi che le
rimbalzavano dalla barra (NewSchedaService, CompleteInspectionService) non
hanno piu' mittente e se ne vanno.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-17 12:47:06 +02:00
parent 101b68123c
commit 2e3a318132
9 changed files with 254 additions and 266 deletions
+7 -172
View File
@@ -1,15 +1,3 @@
@using CommunityToolkit.Mvvm.Messaging
@using SteUp.Shared.Components.Layout.Sheet
@using SteUp.Shared.Components.SingleElements.MessageBox
@using SteUp.Shared.Core.Enum
@using SteUp.Shared.Core.Interface.LocalDb
@using SteUp.Shared.Core.Messages.Ispezione
@using SteUp.Shared.Core.Messages.Scheda
@inject INetworkService NetworkService
@inject IDialogService Dialog
@inject IMessenger Messenger
@inject IIspezioniService IspezioniService
@inject IJSRuntime Js
@implements IDisposable
@*
@@ -17,6 +5,10 @@
sotto, le destinazioni restano a portata di pollice. Icona outline a
riposo, piena da selezionata, etichetta sempre visibile in entrambe.
Da 720px diventa una colonna laterale (vedi components.css).
Qui stanno SOLO le destinazioni. Le azioni di una schermata appartengono
alla schermata, e si trovano nella sua testata: un "+" incastrato nella
barra non dice cosa crea, e cambia significato da una pagina all'altra.
*@
<nav class="navbar @(IsVisible ? "" : "navbar--hidden")" aria-label="Navigazione principale">
@@ -33,189 +25,32 @@
</span>
<span class="navbar__label">Profilo</span>
</NavLink>
@if (ActionsVisible)
{
<button type="button" class="navbar__fab" @onclick="OpenActions"
aria-label="Azioni" title="Azioni">
<i class="ri-add-line" aria-hidden="true"></i>
</button>
}
</nav>
<ActionSheet @bind-Open="_sheetOpen" Title="@SheetTitle" Subtitle="@SheetSubtitle">
<div class="stack stack-xs">
@if (OnInspectionPage)
{
@if (!_isCompleted)
{
<button type="button" class="action-tile" disabled="@(!NetworkService.IsNetworkAvailable())"
@onclick="NewScheda">
<span class="action-tile__icon action-tile__icon--primary">
<i class="ri-add-line" aria-hidden="true"></i>
</span>
<span class="action-tile__label">Nuova scheda</span>
</button>
}
@if (ShowCompleteInspection)
{
<button type="button" class="action-tile" disabled="@(!NetworkService.IsNetworkAvailable())"
@onclick="CompleteInspection">
<span class="action-tile__icon action-tile__icon--success">
<i class="ri-check-double-line" aria-hidden="true"></i>
</span>
<span class="action-tile__label">Concludi ispezione</span>
</button>
}
<button type="button" class="action-tile" disabled="@(!NetworkService.IsNetworkAvailable())"
@onclick="DeleteInspection">
<span class="action-tile__icon action-tile__icon--danger">
<i class="ri-delete-bin-line" aria-hidden="true"></i>
</span>
<span class="action-tile__label">Elimina ispezione</span>
</button>
}
else
{
<button type="button" class="action-tile" disabled="@(!NetworkService.IsNetworkAvailable())"
@onclick="NewInspection">
<span class="action-tile__icon action-tile__icon--primary">
<i class="ri-add-line" aria-hidden="true"></i>
</span>
<span class="action-tile__label">Nuova ispezione</span>
</button>
}
@if (!NetworkService.IsNetworkAvailable())
{
<div class="feedback feedback--warning" role="status">
<i class="feedback__icon ri-wifi-off-line" aria-hidden="true"></i>
<span>Senza connessione queste azioni non sono disponibili. Il lavoro gia' fatto resta sul dispositivo.</span>
</div>
}
</div>
</ActionSheet>
<ConfirmMessageBox @ref="_messageBox" NoText="Annulla" YesText="Concludi" YesColor="Color.Primary"
Message="Una volta conclusa, l'ispezione non sara' piu' modificabile e non potrai aggiungere altre schede. Procedere?"/>
@code
{
private bool IsVisible { get; set; } = true;
private bool ActionsVisible { get; set; } = true;
private bool OnInspectionPage { get; set; }
private bool ShowCompleteInspection { get; set; }
private bool _isCompleted;
private bool _sheetOpen;
private ConfirmMessageBox _messageBox = null!;
private static readonly string[] NavigableRoutes = ["ispezioni", "ispezione", "user"];
private string SheetTitle => OnInspectionPage ? "Azioni ispezione" : "Nuova ispezione";
private string? SheetSubtitle => OnInspectionPage
? SteupDataService.InspectionPageState.Ispezione.CodMdep
: null;
private string CurrentRoute =>
NavigationManager.Uri.Remove(0, NavigationManager.BaseUri.Length).Split('?')[0];
private bool IsOn(string route) => CurrentRoute.EqualsIgnoreCase(route);
protected override Task OnInitializedAsync()
protected override void OnInitialized()
{
NavigationManager.LocationChanged += OnLocationChanged;
Refresh();
return Task.CompletedTask;
}
private void OnLocationChanged(object? sender, LocationChangedEventArgs args)
{
// Cambiando schermata un menu aperto non ha piu' senso.
_sheetOpen = false;
Refresh();
}
private void OnLocationChanged(object? sender, LocationChangedEventArgs args) => Refresh();
private void Refresh()
{
var route = CurrentRoute;
IsVisible = NavigableRoutes.Contains(route, StringComparer.OrdinalIgnoreCase);
OnInspectionPage = route.EqualsIgnoreCase("ispezione");
ActionsVisible = IsVisible && !route.EqualsIgnoreCase("user");
if (OnInspectionPage)
{
var ispezione = SteupDataService.InspectionPageState.Ispezione;
_isCompleted = ispezione.Stato == StatusEnum.Completata;
// "Concludi" non ha senso su un'ispezione mai inviata o gia' conclusa.
ShowCompleteInspection = !ispezione.ActivityId.IsNullOrEmpty() && !_isCompleted;
// Su un'ispezione conclusa resta solo la consultazione.
if (_isCompleted) ActionsVisible = false;
}
IsVisible = NavigableRoutes.Contains(CurrentRoute, StringComparer.OrdinalIgnoreCase);
StateHasChanged();
}
private void OpenActions()
{
Refresh();
_sheetOpen = true;
}
private async Task NewInspection()
{
_sheetOpen = false;
if (await SteupDataService.CanOpenNewInspection())
{
_ = Dialog.OpenSelectShop();
}
else
{
await Dialog.ShowWarning(
"Per aprire una nuova ispezione devi prima concludere tutte le ispezioni aperte da piu' di 20 giorni.");
}
}
private async Task CompleteInspection()
{
_sheetOpen = false;
var result = await _messageBox.ShowAsync();
if (result is true) Messenger.Send(new CompleteInspectionMessage());
}
private async Task NewScheda()
{
_sheetOpen = false;
var ispezione = SteupDataService.InspectionPageState.Ispezione;
var modal = await Dialog.OpenFormScheda(ispezione.CodMdep, ispezione.Data, true);
if (modal is { Canceled: false }) Messenger.Send(new NewSchedaMessage());
}
private async Task DeleteInspection()
{
_sheetOpen = false;
try
{
var ispezione = SteupDataService.InspectionPageState.Ispezione;
await IspezioniService.DeleteIspezioneAsync(ispezione.CodMdep, ispezione.Data, ispezione.Rilevatore);
await Js.InvokeVoidAsync("goBack");
}
catch (Exception e)
{
await Dialog.ShowError(e.Message);
}
}
public void Dispose() => NavigationManager.LocationChanged -= OnLocationChanged;
}
+115 -21
View File
@@ -2,6 +2,7 @@
@using Microsoft.Extensions.Logging
@using SteUp.Shared.Components.Layout
@using SteUp.Shared.Components.Layout.Overlay
@using SteUp.Shared.Components.Layout.Sheet
@using SteUp.Shared.Components.SingleElements
@using SteUp.Shared.Components.SingleElements.Card
@using SteUp.Shared.Components.SingleElements.MessageBox
@@ -12,18 +13,66 @@
@using SteUp.Shared.Core.Interface.IntegryApi
@using SteUp.Shared.Core.Interface.LocalDb
@using SteUp.Shared.Core.Interface.System
@using SteUp.Shared.Core.Messages.Ispezione
@using SteUp.Shared.Core.Messages.Scheda
@inject NewSchedaService NewScheda
@inject CompleteInspectionService CompleteInspection
@inject IIspezioniService IspezioniService
@inject IDialogService Dialog
@inject IIntegrySteupService IntegrySteupService
@inject IFileManager FileManager
@inject ILogger<IspezionePage> Logger
@implements IDisposable
<HeaderLayout Title="@Titolo" Subtitle="@Sottotitolo" Back="true"/>
<HeaderLayout Title="@Titolo" Subtitle="@Sottotitolo" Back="true">
<Actions>
@* Su un'ispezione conclusa resta solo la consultazione: niente azioni. *@
@if (!IsCompleted)
{
@* Qui le azioni sono piu' d'una e non hanno lo stesso peso: lo
sheet le mostra con nome, icona e tono, invece di nasconderle
tutte dietro lo stesso glifo. *@
<button type="button" class="appbar__icon-action" @onclick="@(() => _actionsOpen = true)"
aria-label="Azioni sull'ispezione" title="Azioni">
<i class="ri-more-2-fill" aria-hidden="true"></i>
</button>
}
</Actions>
</HeaderLayout>
<ActionSheet @bind-Open="_actionsOpen" Title="Azioni ispezione" Subtitle="@Ispezione.CodMdep">
<div class="stack stack-xs">
<button type="button" class="action-tile" disabled="@(!NetworkService.IsNetworkAvailable())"
@onclick="NewScheda">
<span class="action-tile__icon action-tile__icon--primary">
<i class="ri-add-line" aria-hidden="true"></i>
</span>
<span class="action-tile__label">Nuova scheda</span>
</button>
@if (!Ispezione.ActivityId.IsNullOrEmpty())
{
<button type="button" class="action-tile" disabled="@(!NetworkService.IsNetworkAvailable())"
@onclick="ConfirmCompleteInspection">
<span class="action-tile__icon action-tile__icon--success">
<i class="ri-check-double-line" aria-hidden="true"></i>
</span>
<span class="action-tile__label">Concludi ispezione</span>
</button>
}
<button type="button" class="action-tile" disabled="@(!NetworkService.IsNetworkAvailable())"
@onclick="ConfirmDeleteInspection">
<span class="action-tile__icon action-tile__icon--danger">
<i class="ri-delete-bin-line" aria-hidden="true"></i>
</span>
<span class="action-tile__label">Elimina ispezione</span>
</button>
@if (!NetworkService.IsNetworkAvailable())
{
<div class="feedback feedback--warning" role="status">
<i class="feedback__icon ri-wifi-off-line" aria-hidden="true"></i>
<span>Senza connessione queste azioni non sono disponibili. Il lavoro gia' fatto resta sul dispositivo.</span>
</div>
}
</div>
</ActionSheet>
<div class="app-scroll">
<div class="page-width page-body stack stack-lg">
@@ -92,8 +141,16 @@
@if (!IsCompleted)
{
<p class="empty-state__text">
Tocca <b>+</b> nella barra in basso per creare la prima scheda di questa ispezione.
Aggiungi la prima scheda di questa ispezione.
</p>
<div class="empty-state__action">
<MudButton Variant="Variant.Filled" Color="Color.Primary"
StartIcon="@Icons.Material.Rounded.Add"
Disabled="@(!NetworkService.IsNetworkAvailable())"
OnClick="NewScheda">
Nuova scheda
</MudButton>
</div>
}
</div>
}
@@ -149,7 +206,14 @@
</div>
<SpinnerOverlay VisibleOverlay="VisibleOverlay" SuccessAnimation="_success" Message="@_progressMessage"/>
<ConfirmMessageBox @ref="_exportRepartoBox" NoText="Annulla" YesText="Invia" YesColor="Color.Primary"
<ConfirmMessageBox @ref="_completeBox" Title="Concludere l'ispezione?" NoText="Annulla"
YesText="Concludi" YesColor="Color.Primary"
Message="Una volta conclusa, l'ispezione non sarà più modificabile e non potrai aggiungere altre schede."/>
<ConfirmMessageBox @ref="_deleteBox" Title="Eliminare l'ispezione?" NoText="Annulla"
YesText="Elimina" YesColor="Color.Error"
Message="L'ispezione e tutte le sue schede verranno eliminate dal dispositivo. L'operazione non è reversibile."/>
<ConfirmMessageBox @ref="_exportRepartoBox" Title="Inviare le schede del reparto?" NoText="Annulla"
YesText="Invia" YesColor="Color.Primary"
Message="Le schede complete di questo reparto verranno inviate al server. Procedere?"/>
@code {
@@ -162,6 +226,10 @@
private string? _progressMessage;
private ConfirmMessageBox _exportRepartoBox = null!;
private ConfirmMessageBox _completeBox = null!;
private ConfirmMessageBox _deleteBox = null!;
private bool _actionsOpen;
private Ispezione Ispezione => SteupDataService.InspectionPageState.Ispezione;
private bool IsCompleted => Ispezione.Stato == StatusEnum.Completata;
@@ -185,13 +253,7 @@
private string Sottotitolo => Ispezione.CodMdep;
protected override void OnInitialized()
{
NewScheda.OnNewScheda += LoadSchede;
CompleteInspection.OnComplete += HandleCompleteInspection;
LoadSchede();
}
protected override void OnInitialized() => LoadSchede();
private void LoadSchede()
{
@@ -208,6 +270,44 @@
});
}
private async Task NewScheda()
{
_actionsOpen = false;
var modal = await Dialog.OpenFormScheda(Ispezione.CodMdep, Ispezione.Data, true);
if (modal is { Canceled: false }) LoadSchede();
}
private async Task ConfirmCompleteInspection()
{
_actionsOpen = false;
var confirmed = await _completeBox.ShowAsync();
if (confirmed is true) HandleCompleteInspection();
}
private async Task ConfirmDeleteInspection()
{
_actionsOpen = false;
// Prima non c'era conferma: un tocco cancellava l'ispezione e tutte le
// sue schede senza appello.
var confirmed = await _deleteBox.ShowAsync();
if (confirmed is not true) return;
try
{
var ispezione = Ispezione;
await IspezioniService.DeleteIspezioneAsync(ispezione.CodMdep, ispezione.Data, ispezione.Rilevatore);
NavigationManager.NavigateTo("/ispezioni");
}
catch (Exception e)
{
OnError(e, e.Message);
}
}
private async void HandleCompleteInspection()
{
// Guardia anti doppio-invio: il completamento è irreversibile.
@@ -496,10 +596,4 @@
);
}
void IDisposable.Dispose()
{
NewScheda.OnNewScheda -= LoadSchede;
CompleteInspection.OnComplete -= HandleCompleteInspection;
}
}
@@ -5,8 +5,21 @@
@using SteUp.Shared.Core.Entities
@using SteUp.Shared.Core.Interface.LocalDb
@inject IIspezioniService IspezioniService
@inject IDialogService Dialog
<HeaderLayout Title="Ispezioni" Subtitle="@Subtitle"/>
<HeaderLayout Title="Ispezioni" Subtitle="@Subtitle">
<Actions>
@* L'unica azione della schermata: porta dritta alla scelta del punto
vendita. Un passaggio intermedio per scegliere fra una cosa sola non
e' una scelta, e' un tocco in piu'. *@
<button type="button" class="appbar__action" @onclick="NewInspection"
disabled="@(!NetworkService.IsNetworkAvailable())"
title="@(NetworkService.IsNetworkAvailable() ? null : "Serve la connessione per aprire una nuova ispezione")">
<i class="ri-add-line" aria-hidden="true"></i>
<span>Nuova</span>
</button>
</Actions>
</HeaderLayout>
<div class="app-scroll">
<div class="page-width page-body">
@@ -32,8 +45,16 @@
<i class="empty-state__icon ri-clipboard-line" aria-hidden="true"></i>
<p class="empty-state__title">Nessuna ispezione</p>
<p class="empty-state__text">
Tocca <b>+</b> nella barra in basso per aprire la prima ispezione su un punto vendita.
Apri la prima ispezione scegliendo il punto vendita da controllare.
</p>
<div class="empty-state__action">
<MudButton Variant="Variant.Filled" Color="Color.Primary"
StartIcon="@Icons.Material.Rounded.Add"
Disabled="@(!NetworkService.IsNetworkAvailable())"
OnClick="NewInspection">
Nuova ispezione
</MudButton>
</div>
</div>
}
else
@@ -65,6 +86,22 @@
StateHasChanged();
}
private async Task NewInspection()
{
if (await SteupDataService.CanOpenNewInspection())
{
var modal = await Dialog.OpenSelectShop();
// La scelta del punto vendita naviga da sola sull'ispezione; se
// l'utente torna indietro, la lista dev'essere quella aggiornata.
if (modal is { Canceled: true }) return;
}
else
{
await Dialog.ShowWarning(
"Per aprire una nuova ispezione devi prima concludere tutte le ispezioni aperte da piu' di 20 giorni.");
}
}
private void OnClickIspezione(Ispezione ispezione)
{
SteupDataService.InspectionPageState.Ispezione = ispezione;